Skip to content

Commit 4ecad38

Browse files
committed
Add individual product pages with pen-color selection
- New product.html template (?id=<key>) rendering any edition from data - Per-edition live-plot preview that strokes the path in the chosen pen color on a Strathmore Bristol paper background - Pen color variants (black/red/blue) at the same price, chosen on the product page; choice flows cart → Stripe line item → order email - Cart now keys line items by edition+color and shows the ink in the drawer - Worker validates color against an allow-list; price stays server-side - Shop grid cards link through to their product page - Add per-edition lore blurbs + materials/shipping detail to product pages https://claude.ai/code/session_01VhWRYXbuqapZ9YkvksroaS
1 parent 9905be5 commit 4ecad38

10 files changed

Lines changed: 419 additions & 24 deletions

File tree

data/editions.js

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

product.html

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<!-- Google tag (gtag.js) -->
6+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-1HJ4VQ4K1B"></script>
7+
<script>
8+
window.dataLayer = window.dataLayer || [];
9+
function gtag(){dataLayer.push(arguments);}
10+
gtag('js', new Date());
11+
gtag('config', 'G-1HJ4VQ4K1B');
12+
</script>
13+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
14+
<title>Edition — PLOTFLOW*</title>
15+
<link rel="preconnect" href="https://fonts.googleapis.com">
16+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17+
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,600&family=Noto+Sans+JP:wght@500;700;900&display=swap" rel="stylesheet">
18+
<link rel="stylesheet" href="styles/tokens.css">
19+
<link rel="stylesheet" href="styles/styles.css">
20+
<meta name="description" content="A made-to-order, machine-drawn PLOTFLOW edition — drawn in ink on Strathmore Bristol, signed and numbered.">
21+
<link rel="canonical" href="https://plotflow.io/product.html">
22+
<meta name="theme-color" content="#43463e">
23+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
24+
<meta property="og:type" content="product">
25+
<meta property="og:site_name" content="PLOTFLOW">
26+
<meta property="og:title" content="Edition — PLOTFLOW*">
27+
<meta property="og:description" content="A made-to-order, machine-drawn PLOTFLOW edition — drawn in ink on Strathmore Bristol, signed and numbered.">
28+
<meta property="og:url" content="https://plotflow.io/product.html">
29+
<meta property="og:image" content="https://plotflow.io/assets/og.png">
30+
<meta name="twitter:card" content="summary_large_image">
31+
<meta name="twitter:title" content="Edition — PLOTFLOW*">
32+
<meta name="twitter:description" content="A made-to-order, machine-drawn PLOTFLOW edition — drawn in ink on Strathmore Bristol, signed and numbered.">
33+
<meta name="twitter:image" content="https://plotflow.io/assets/og.png">
34+
</head>
35+
<body>
36+
37+
<nav><div class="nav-in">
38+
<a href="/" class="brand">Plotflow<i>*</i></a>
39+
<div style="display:flex;align-items:center;gap:16px">
40+
<div class="nav-links"><a href="/#shop">The Work</a><a href="/#process">Process</a><a href="/#feature">Live Plot</a></div>
41+
<button class="cart">Cart [0]</button>
42+
</div>
43+
</div></nav>
44+
45+
<section class="page">
46+
<div class="wrap">
47+
<a class="pd-back tiny" href="/#shop">← All editions</a>
48+
49+
<div class="pd" id="pd" hidden>
50+
<!-- LEFT: live plot preview on Strathmore Bristol -->
51+
<div class="pd-stage-wrap">
52+
<div class="pd-stage" id="pdStage">
53+
<span class="pd-bedlabel tiny" id="pdBedlabel">BED 01</span>
54+
<canvas id="pdCanvas"></canvas>
55+
<svg id="pdSvg" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><path id="pdPath" d=""></path></svg>
56+
<button class="pd-replay" id="pdReplay" hidden>▶&#xFE0E; Watch it plot</button>
57+
</div>
58+
<div class="pd-stage-meta tiny"><span id="pdInk">マシンドロー</span><span id="pdSize"></span></div>
59+
</div>
60+
61+
<!-- RIGHT: details + acquire -->
62+
<div class="pd-info">
63+
<div class="pd-head">
64+
<div class="pd-code tiny" id="pdCode"></div>
65+
<h1 class="pd-name" id="pdName"></h1>
66+
<div class="jp pd-jp" id="pdJp"></div>
67+
</div>
68+
69+
<div class="pd-edrow">
70+
<span class="pd-ed tiny" id="pdEd"></span>
71+
<span class="pd-price" id="pdPrice"></span>
72+
</div>
73+
74+
<p class="pd-lore" id="pdLore"></p>
75+
76+
<div class="pd-field">
77+
<span class="pd-field-label tiny">Pen color</span>
78+
<div class="pd-swatches" id="pdSwatches" role="radiogroup" aria-label="Pen color"></div>
79+
</div>
80+
81+
<button class="pd-acquire" id="pdAcquire">Acquire — <span id="pdAcquirePrice"></span></button>
82+
<p class="pd-note tiny">Plotted to order in your chosen ink. Shipping + any taxes calculated at checkout.</p>
83+
84+
<div class="pd-specs">
85+
<h3>Materials &amp; specs</h3>
86+
<ul>
87+
<li><strong>Paper</strong> — Strathmore 300 Series Bristol, smooth, 11×14″</li>
88+
<li><strong>Ink</strong> — Staedtler Triplus 0.3mm fineliner, pigment-based &amp; lightfast</li>
89+
<li><strong>Made by</strong> — AxiDraw pen plotter, drawn line by line</li>
90+
<li><strong>Finishing</strong> — Inspected, signed, and numbered by hand</li>
91+
</ul>
92+
<h3>Shipping &amp; returns</h3>
93+
<p>Each edition is plotted to order — please allow 7–14 days for production before dispatch. U.S. shipping is included; international calculated at checkout. Damaged-in-transit pieces are replaced free. See <a href="shipping.html">Shipping</a> &amp; <a href="returns.html">Returns</a>.</p>
94+
</div>
95+
</div>
96+
</div>
97+
98+
<div class="pd-missing" id="pdMissing" hidden>
99+
<div class="page-head"><h1>Not found<span class="star">*</span></h1><div class="jp">作品が見つかりません</div></div>
100+
<div class="page-body"><p>That edition isn't part of the current run. <a href="/#shop">Browse all editions →</a></p></div>
101+
</div>
102+
</div>
103+
</section>
104+
105+
<footer>
106+
<div class="grain"></div>
107+
<div class="wrap">
108+
<div class="foot-mast">PLOTFLOW<span class="s">*</span></div>
109+
<div class="foot-cols">
110+
<div class="fc"><h5>Shop*</h5><a href="/#shop">All editions</a><a href="commissions.html">Commissions</a></div>
111+
<div class="fc"><h5>Studio*</h5><a href="/#process">Process</a><a href="about.html">About</a><a href="contact.html">Contact</a></div>
112+
<div class="fc"><h5>Info*</h5><a href="shipping.html">Shipping</a><a href="returns.html">Returns</a><a href="faq.html">FAQ</a></div>
113+
<div class="fc"><h5>マシンドロー</h5><a href="https://instagram.com/plotflow" target="_blank" rel="noopener">@plotflow</a><a href="mailto:hello@plotflow.io?subject=Newsletter">Newsletter</a><span class="est">est. 2026</span></div>
114+
</div>
115+
<div class="foot-bot tiny"><span>© 2026 PLOTFLOW* — PLOTTED IN THE U.S.A.</span><span>マシンドロー · DRAWN BY MACHINE</span></div>
116+
</div>
117+
</footer>
118+
119+
<!-- data first, then components -->
120+
<script src="data/editions.js"></script>
121+
<script src="scripts/config.js"></script>
122+
<script src="scripts/product.js"></script>
123+
<script src="scripts/cart.js"></script>
124+
</body>
125+
</html>

scripts/cart.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
88
Cart items carry their own display fields (name/code/price/edition)
99
captured at add-time, so interior pages don't need data/editions.js.
10+
Each item also carries a pen `color` (black/red/blue); the same suit
11+
in two colors is two distinct line items, identified by key|color.
1012
============================================================ */
1113
(function () {
1214
var KEY = "plotflow_cart_v1";
1315
var CFG = window.PLOTFLOW_CONFIG || {};
1416
var CUR = CFG.currency || "$";
1517
var DATA = (window.PLOTFLOW && window.PLOTFLOW.suits) || {};
18+
var COLORS = { black: "Black", red: "Red", blue: "Blue" };
19+
20+
function normColor(c) { c = String(c || "black").toLowerCase(); return COLORS[c] ? c : "black"; }
21+
function colorName(c) { return COLORS[normColor(c)]; }
22+
function lineId(i) { return i.key + "|" + normColor(i.color); }
1623

1724
function read() {
1825
try { return JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { return []; }
@@ -27,23 +34,24 @@
2734
function count() { return read().reduce(function (n, i) { return n + i.qty; }, 0); }
2835
function subtotal() { return read().reduce(function (s, i) { return s + priceNum(i.price) * i.qty; }, 0); }
2936

30-
function add(key) {
37+
function add(key, color) {
38+
color = normColor(color);
3139
var items = read();
32-
var found = items.filter(function (i) { return i.key === key; })[0];
40+
var found = items.filter(function (i) { return i.key === key && normColor(i.color) === color; })[0];
3341
if (found) { found.qty++; }
3442
else {
3543
var s = DATA[key] || {};
36-
items.push({ key: key, qty: 1, name: s.name || key, code: s.code || "", price: s.price || "", edition: s.edition || "" });
44+
items.push({ key: key, color: color, qty: 1, name: s.name || key, code: s.code || "", price: s.price || "", edition: s.edition || "" });
3745
}
3846
write(items);
3947
openDrawer();
4048
}
41-
function setQty(key, qty) {
42-
var items = read().map(function (i) { return i.key === key ? Object.assign({}, i, { qty: qty }) : i; })
49+
function setQty(id, qty) {
50+
var items = read().map(function (i) { return lineId(i) === id ? Object.assign({}, i, { qty: qty }) : i; })
4351
.filter(function (i) { return i.qty > 0; });
4452
write(items);
4553
}
46-
function remove(key) { write(read().filter(function (i) { return i.key !== key; })); }
54+
function remove(id) { write(read().filter(function (i) { return lineId(i) !== id; })); }
4755
function clear() { write([]); }
4856

4957
/* ---- drawer markup (built once, appended to <body>) ---- */
@@ -78,12 +86,12 @@
7886
checkoutBtn.addEventListener("click", checkout);
7987

8088
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);
89+
var row = e.target.closest("[data-id]"); if (!row) return;
90+
var id = row.getAttribute("data-id");
91+
var item = read().filter(function (i) { return lineId(i) === id; })[0]; if (!item) return;
92+
if (e.target.closest("[data-inc]")) setQty(id, item.qty + 1);
93+
else if (e.target.closest("[data-dec]")) setQty(id, item.qty - 1);
94+
else if (e.target.closest("[data-rm]")) remove(id);
8795
});
8896

8997
document.addEventListener("keydown", function (e) { if (e.key === "Escape") closeDrawer(); });
@@ -101,9 +109,9 @@
101109
itemsEl.innerHTML = '<div class="cart-empty tiny">Your cart is empty.</div>';
102110
} else {
103111
itemsEl.innerHTML = items.map(function (i) {
104-
return '<div class="ci" data-key="' + i.key + '">' +
112+
return '<div class="ci" data-id="' + lineId(i) + '">' +
105113
'<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>' +
114+
'<div class="ci-ed tiny">' + (i.edition ? i.edition + " · " : "") + colorName(i.color) + ' ink</div></div>' +
107115
'<div class="ci-qty"><button data-dec aria-label="Decrease">−</button>' +
108116
'<span>' + i.qty + '</span>' +
109117
'<button data-inc aria-label="Increase">+</button></div>' +
@@ -128,7 +136,7 @@
128136
fetch(CFG.checkoutEndpoint, {
129137
method: "POST",
130138
headers: { "Content-Type": "application/json" },
131-
body: JSON.stringify({ items: items.map(function (i) { return { key: i.key, qty: i.qty }; }) })
139+
body: JSON.stringify({ items: items.map(function (i) { return { key: i.key, qty: i.qty, color: normColor(i.color) }; }) })
132140
})
133141
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
134142
.then(function (res) {
@@ -144,7 +152,7 @@
144152
/* ---- global wiring ---- */
145153
document.addEventListener("click", function (e) {
146154
var acq = e.target.closest("[data-acq]");
147-
if (acq) { e.preventDefault(); add(acq.getAttribute("data-acq")); return; }
155+
if (acq) { e.preventDefault(); add(acq.getAttribute("data-acq"), acq.getAttribute("data-color")); return; }
148156
var cartBtn = e.target.closest(".cart");
149157
if (cartBtn) { e.preventDefault(); openDrawer(); }
150158
});

0 commit comments

Comments
 (0)