Skip to content

Commit e8e53e3

Browse files
ddyson1claude
andauthored
Multi-item cart + Stripe checkout, mobile hero layout, copy tweak (#5)
- Cart: localStorage cart, slide-out drawer, nav badge, per-edition Acquire buttons; checkout posts keys+qty to a Cloudflare Worker that resolves prices server-side and creates a Stripe Checkout Session. Adds success.html, scripts/config.js, scripts/cart.js, and worker/. - Mobile: taller portrait plot bed and reordered hero stack so the controls no longer overlap. - Copy: 'avant-garde' -> 'independent'. https://claude.ai/code/session_01VhWRYXbuqapZ9YkvksroaS Co-authored-by: Claude <noreply@anthropic.com>
1 parent 141f1d5 commit e8e53e3

18 files changed

Lines changed: 474 additions & 4 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@
44
*~
55
.env
66
Thumbs.db
7+
8+
# Cloudflare Worker
9+
worker/node_modules/
10+
worker/.wrangler/
11+
.dev.vars

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PLOTFLOW
22

3-
**Avant-garde pen-plotter studio.** Universal-Century mobile suits, rebuilt as a single unbroken line and drawn in ink by machine.
3+
**Independent pen-plotter studio.** Universal-Century mobile suits, rebuilt as a single unbroken line and drawn in ink by machine.
44

55
一筆書き — *hitofudegaki*, "one continuous line."
66

commissions.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,7 @@ <h3>Get Started</h3>
7070
</div>
7171
</footer>
7272

73+
<script src="scripts/config.js"></script>
74+
<script src="scripts/cart.js"></script>
7375
</body>
7476
</html>

contact.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,7 @@ <h3>Studio</h3>
6767
</div>
6868
</footer>
6969

70+
<script src="scripts/config.js"></script>
71+
<script src="scripts/cart.js"></script>
7072
</body>
7173
</html>

faq.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,7 @@ <h3>Can I return a piece?</h3>
9494
</div>
9595
</footer>
9696

97+
<script src="scripts/config.js"></script>
98+
<script src="scripts/cart.js"></script>
9799
</body>
98100
</html>

index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
<div class="hero-head">
2929
<h1>PLOTFLOW<span class="s">*</span></h1>
30-
<div class="sub">avant-garde pen-plotter studio <b>//</b> mobile suit editions, drawn by machine <b>//</b> プロットフロー</div>
30+
<div class="sub">independent pen-plotter studio <b>//</b> mobile suit editions, drawn by machine <b>//</b> プロットフロー</div>
3131
</div>
3232

3333
<div class="hud num">
@@ -118,7 +118,9 @@ <h2>The Work<span class="star">*</span></h2><span class="jp">作品</span>
118118

119119
<!-- data first, then components -->
120120
<script src="data/editions.js"></script>
121+
<script src="scripts/config.js"></script>
121122
<script src="scripts/shop.js"></script>
122123
<script src="scripts/plotter.js"></script>
124+
<script src="scripts/cart.js"></script>
123125
</body>
124126
</html>

manifesto.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,7 @@ <h1>Manifesto<span class="star">*</span></h1>
6464
</div>
6565
</footer>
6666

67+
<script src="scripts/config.js"></script>
68+
<script src="scripts/cart.js"></script>
6769
</body>
6870
</html>

preview.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@
178178

179179
<div class="hero-head">
180180
<h1>PLOTFLOW<span class="s">*</span></h1>
181-
<div class="sub">avant-garde pen-plotter studio <b>//</b> mobile suit editions, drawn by machine <b>//</b> プロットフロー</div>
181+
<div class="sub">independent pen-plotter studio <b>//</b> mobile suit editions, drawn by machine <b>//</b> プロットフロー</div>
182182
</div>
183183

184184
<div class="hud num">

returns.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,7 @@ <h3>Cancellations</h3>
6060
</div>
6161
</footer>
6262

63+
<script src="scripts/config.js"></script>
64+
<script src="scripts/cart.js"></script>
6365
</body>
6466
</html>

scripts/cart.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* ============================================================
2+
PLOTFLOW · Cart
3+
A client-side, multi-item cart persisted in localStorage. The
4+
"Acquire" buttons add editions; the nav "Cart [N]" opens a drawer.
5+
Checkout posts {items:[{key,qty}]} to the Cloudflare Worker, which
6+
builds a Stripe Checkout Session and returns its hosted URL.
7+
8+
Cart items carry their own display fields (name/code/price/edition)
9+
captured at add-time, so interior pages don't need data/editions.js.
10+
============================================================ */
11+
(function () {
12+
var KEY = "plotflow_cart_v1";
13+
var CFG = window.PLOTFLOW_CONFIG || {};
14+
var CUR = CFG.currency || "$";
15+
var DATA = (window.PLOTFLOW && window.PLOTFLOW.suits) || {};
16+
17+
function read() {
18+
try { return JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { return []; }
19+
}
20+
function write(items) {
21+
localStorage.setItem(KEY, JSON.stringify(items));
22+
render();
23+
}
24+
function priceNum(s) { return parseFloat(String(s == null ? "" : s).replace(/[^0-9.]/g, "")) || 0; }
25+
function money(n) { return CUR + (Math.round(n * 100) / 100).toLocaleString(); }
26+
27+
function count() { return read().reduce(function (n, i) { return n + i.qty; }, 0); }
28+
function subtotal() { return read().reduce(function (s, i) { return s + priceNum(i.price) * i.qty; }, 0); }
29+
30+
function add(key) {
31+
var items = read();
32+
var found = items.filter(function (i) { return i.key === key; })[0];
33+
if (found) { found.qty++; }
34+
else {
35+
var s = DATA[key] || {};
36+
items.push({ key: key, qty: 1, name: s.name || key, code: s.code || "", price: s.price || "", edition: s.edition || "" });
37+
}
38+
write(items);
39+
openDrawer();
40+
}
41+
function setQty(key, qty) {
42+
var items = read().map(function (i) { return i.key === key ? Object.assign({}, i, { qty: qty }) : i; })
43+
.filter(function (i) { return i.qty > 0; });
44+
write(items);
45+
}
46+
function remove(key) { write(read().filter(function (i) { return i.key !== key; })); }
47+
function clear() { write([]); }
48+
49+
/* ---- drawer markup (built once, appended to <body>) ---- */
50+
var overlay, drawer, itemsEl, subEl, checkoutBtn;
51+
function build() {
52+
overlay = document.createElement("div");
53+
overlay.className = "cart-overlay";
54+
55+
drawer = document.createElement("aside");
56+
drawer.className = "cart-drawer";
57+
drawer.setAttribute("aria-hidden", "true");
58+
drawer.innerHTML =
59+
'<div class="cart-head"><div><span class="cart-title">Cart</span><span class="star cart-ast">*</span>' +
60+
'<span class="jp cart-jp">買い物かご</span></div>' +
61+
'<button class="cart-close" aria-label="Close cart">×</button></div>' +
62+
'<div class="cart-items"></div>' +
63+
'<div class="cart-foot">' +
64+
'<div class="cart-sub"><span class="tiny">Subtotal</span><span class="cart-sub-val">' + money(0) + '</span></div>' +
65+
'<p class="cart-note tiny">Shipping + any taxes calculated at checkout. Each edition is plotted to order.</p>' +
66+
'<button class="cart-checkout">Checkout</button>' +
67+
'</div>';
68+
69+
document.body.appendChild(overlay);
70+
document.body.appendChild(drawer);
71+
72+
itemsEl = drawer.querySelector(".cart-items");
73+
subEl = drawer.querySelector(".cart-sub-val");
74+
checkoutBtn = drawer.querySelector(".cart-checkout");
75+
76+
overlay.addEventListener("click", closeDrawer);
77+
drawer.querySelector(".cart-close").addEventListener("click", closeDrawer);
78+
checkoutBtn.addEventListener("click", checkout);
79+
80+
itemsEl.addEventListener("click", function (e) {
81+
var row = e.target.closest("[data-key]"); if (!row) return;
82+
var k = row.getAttribute("data-key");
83+
var item = read().filter(function (i) { return i.key === k; })[0]; if (!item) return;
84+
if (e.target.closest("[data-inc]")) setQty(k, item.qty + 1);
85+
else if (e.target.closest("[data-dec]")) setQty(k, item.qty - 1);
86+
else if (e.target.closest("[data-rm]")) remove(k);
87+
});
88+
89+
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeDrawer(); });
90+
}
91+
92+
function openDrawer() { if (!drawer) return; drawer.classList.add("open"); overlay.classList.add("show"); drawer.setAttribute("aria-hidden", "false"); }
93+
function closeDrawer() { if (!drawer) return; drawer.classList.remove("open"); overlay.classList.remove("show"); drawer.setAttribute("aria-hidden", "true"); }
94+
95+
function render() {
96+
// nav badges
97+
[].forEach.call(document.querySelectorAll(".cart"), function (b) { b.textContent = "Cart [" + count() + "]"; });
98+
if (!itemsEl) return;
99+
var items = read();
100+
if (!items.length) {
101+
itemsEl.innerHTML = '<div class="cart-empty tiny">Your cart is empty.</div>';
102+
} else {
103+
itemsEl.innerHTML = items.map(function (i) {
104+
return '<div class="ci" data-key="' + i.key + '">' +
105+
'<div class="ci-main"><div class="ci-name">' + (i.code ? i.code + " " : "") + i.name + '</div>' +
106+
'<div class="ci-ed tiny">' + (i.edition || "") + '</div></div>' +
107+
'<div class="ci-qty"><button data-dec aria-label="Decrease">−</button>' +
108+
'<span>' + i.qty + '</span>' +
109+
'<button data-inc aria-label="Increase">+</button></div>' +
110+
'<div class="ci-price">' + money(priceNum(i.price) * i.qty) + '</div>' +
111+
'<button class="ci-rm" data-rm aria-label="Remove">×</button>' +
112+
'</div>';
113+
}).join("");
114+
}
115+
if (subEl) subEl.textContent = money(subtotal());
116+
if (checkoutBtn) checkoutBtn.disabled = items.length === 0;
117+
}
118+
119+
function checkout() {
120+
var items = read();
121+
if (!items.length) return;
122+
if (!CFG.checkoutEndpoint) {
123+
alert("Checkout isn't connected yet. (Set checkoutEndpoint in scripts/config.js once the Stripe Worker is deployed.)");
124+
return;
125+
}
126+
var orig = checkoutBtn.textContent;
127+
checkoutBtn.disabled = true; checkoutBtn.textContent = "Redirecting…";
128+
fetch(CFG.checkoutEndpoint, {
129+
method: "POST",
130+
headers: { "Content-Type": "application/json" },
131+
body: JSON.stringify({ items: items.map(function (i) { return { key: i.key, qty: i.qty }; }) })
132+
})
133+
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
134+
.then(function (res) {
135+
if (res.ok && res.d.url) { window.location.href = res.d.url; }
136+
else { throw new Error((res.d && res.d.error) || "Checkout failed"); }
137+
})
138+
.catch(function (err) {
139+
alert("Could not start checkout. " + err.message);
140+
checkoutBtn.disabled = false; checkoutBtn.textContent = orig;
141+
});
142+
}
143+
144+
/* ---- global wiring ---- */
145+
document.addEventListener("click", function (e) {
146+
var acq = e.target.closest("[data-acq]");
147+
if (acq) { e.preventDefault(); add(acq.getAttribute("data-acq")); return; }
148+
var cartBtn = e.target.closest(".cart");
149+
if (cartBtn) { e.preventDefault(); openDrawer(); }
150+
});
151+
152+
// public API
153+
window.PlotflowCart = { add: add, remove: remove, clear: clear, open: openDrawer, count: count };
154+
155+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", function () { build(); render(); });
156+
else { build(); render(); }
157+
})();

0 commit comments

Comments
 (0)