/* sema ikutoke Collections — Builder, Storefront, Checkout views */ const { useState: useStateB, useEffect: useEffectB, useRef: useRefB, useMemo: useMemoB } = React; /* ─── Block definitions ─── */ const BLOCK_DEFS = { hero: { name: "Hero", glyph: "H", default: { title: "Field Notes — Vol. 03", subtitle: "A small book of photographs and essays from a year spent walking.", emph: "Vol. 03" } }, collection: { name: "Collection", glyph: "C", default: { heading: "What's inside", count: 6, label: "Photographs" } }, buybar: { name: "Buy bar", glyph: "$", default: { title: "Buy the collection", price: "1,200", subtitle: "23 photographs · 14 essays · 42 MB · DRM-free" } }, quote: { name: "Pull-quote", glyph: "❝", default: { text: "A small, quieter way to make a living from your work.", attribution: "— a kind reader" } }, text: { name: "Text", glyph: "T", default: { body: "sema ikutoke is built around the idea that selling work shouldn't get in the way of making it. Write here." } }, gallery: { name: "Gallery", glyph: "▣", default: { count: 4 } }, divider: { name: "Divider", glyph: "—", default: {} }, }; const DEFAULT_PAGE = [ { id: "b1", type: "hero", data: { ...BLOCK_DEFS.hero.default } }, { id: "b2", type: "collection", data: { ...BLOCK_DEFS.collection.default } }, { id: "b3", type: "quote", data: { ...BLOCK_DEFS.quote.default } }, { id: "b4", type: "buybar", data: { ...BLOCK_DEFS.buybar.default } }, ]; /* ─── Rendered block (inside canvas / storefront) ─── */ function CB_Hero({ data, accent }) { return (
EDITION · 2026

{data.title.split(data.emph).map((part, i, arr) => i < arr.length - 1 ? {part}{data.emph} : {part})}

{data.subtitle}

); } function CB_Collection({ data }) { return (

§ {data.heading}

{data.count} {data.label}
{Array.from({length: data.count}).map((_, i) => (
№ {String(i+1).padStart(2,"0")} JPG
))}
); } function CB_Quote({ data }) { return (

"{data.text}"

{data.attribution}
); } function CB_BuyBar({ data, onBuy }) { return (
{data.title}
KSh {data.price} · one-time
{data.subtitle}
); } function CB_Text({ data }) { return (

{data.body}

); } function CB_Gallery({ data }) { return (
{Array.from({length: data.count}).map((_, i) => (
))}
); } function CB_Divider() { return
; } function renderBlock(block, onBuy) { const C = { hero: CB_Hero, collection: CB_Collection, quote: CB_Quote, buybar: CB_BuyBar, text: CB_Text, gallery: CB_Gallery, divider: CB_Divider, }[block.type]; return ; } /* ═══════════════════════════════════════════════════════════════════════ BUILDER ═══════════════════════════════════════════════════════════════════════ */ function Builder({ goto, canvasStyle, page, setPage }) { const [selectedId, setSelectedId] = useStateB(page[0]?.id); const [dragOver, setDragOver] = useStateB(null); // drop-zone index const [draggingType, setDraggingType] = useStateB(null); const [siteName, setSiteName] = useStateB("Field Notes Vol. 03"); const [viewport, setViewport] = useStateB("desktop"); const selected = page.find((b) => b.id === selectedId); const onDragStart = (type) => () => setDraggingType(type); const onDragEnd = () => { setDraggingType(null); setDragOver(null); }; const onDropAt = (idx) => (e) => { e.preventDefault(); if (!draggingType) return; const newBlock = { id: "b" + Date.now(), type: draggingType, data: { ...BLOCK_DEFS[draggingType].default }, }; const next = [...page]; next.splice(idx, 0, newBlock); setPage(next); setSelectedId(newBlock.id); onDragEnd(); }; const deleteBlock = (id) => { setPage(page.filter((b) => b.id !== id)); if (selectedId === id && page.length > 1) setSelectedId(page[0].id); }; const updateSelected = (key, value) => { setPage(page.map((b) => b.id === selectedId ? { ...b, data: { ...b.data, [key]: value } } : b)); }; return (
Workspace ▸ Collections ▸ setSiteName(e.target.value)} /> ● Saved
{/* LEFT: blocks */} {/* CENTER: canvas */}
https://{siteName.toLowerCase().replace(/\s+/g,"-")}.sema-ikutoke.so · LIVE
{ e.preventDefault(); setDragOver(0); }} onDragLeave={() => setDragOver(null)} onDrop={onDropAt(0)} hasBlocks={page.length > 0} /> {page.map((block, i) => (
setSelectedId(block.id)} > {renderBlock(block, () => {})}
{ e.preventDefault(); setDragOver(i+1); }} onDragLeave={() => setDragOver(null)} onDrop={onDropAt(i+1)} hasBlocks={true} />
))}
{/* RIGHT: inspector */}