/* 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 (
);
}
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 (
{/* 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 */}
);
}
function DropZone({ active, onDragOver, onDragLeave, onDrop, hasBlocks, index }) {
return (
{active ? "Drop here" : (index === 0 && !hasBlocks ? "Drag a block to start" : "+ drop block")}
);
}
/* ═══════════════════════════════════════════════════════════════════════
STOREFRONT (the published site, with Buy)
═══════════════════════════════════════════════════════════════════════ */
function Storefront({ goto, page, onBuy }) {
return (
▸ kamau.sema-ikutoke.so · viewing as buyer · ● LIVE
https://kamau.sema-ikutoke.so
{page.map((block, i) => (
{renderBlock(block, onBuy)}
))}
);
}
/* ═══════════════════════════════════════════════════════════════════════
CHECKOUT — variants: drawer / modal / page
═══════════════════════════════════════════════════════════════════════ */
function useMpesaFlow(onSuccess) {
const [stage, setStage] = useStateB("entry"); // entry → sending → awaiting → success
const [phone, setPhone] = useStateB("712 345 678");
const [code, setCode] = useStateB("+254");
const submit = () => {
setStage("sending");
setTimeout(() => setStage("awaiting"), 900);
setTimeout(() => setStage("success"), 3400);
};
const reset = () => setStage("entry");
return { stage, phone, code, setPhone, setCode, submit, reset };
}
function MpesaFlow({ flow, item, onClose, onSuccess }) {
const { stage, phone, code, setPhone, setCode, submit } = flow;
if (stage === "entry") {
return (
<>
M-PESA
STK Push
Enter the phone to charge.
We'll send an M-PESA prompt to this number. You'll have 60 seconds to approve it.
{item.name}KSh {item.price}
M-PESA feeKSh 0
TotalKSh {item.price}
By tapping you agree to sema ikutoke's terms · Safaricom M-PESA Daraja
>
);
}
if (stage === "sending") {
return (
📲
Sending to your phone…
Reaching out to {code} {phone}.
);
}
if (stage === "awaiting") {
return (
↗
Approve on your phone.
Enter your M-PESA PIN on the prompt we sent to {code} {phone}.
Prompt refRST-9F2-{Math.floor(Math.random()*9000+1000)}
AmountKSh {item.price}
Expires60 s
);
}
// success
return (
✓
You've got it.
Receipt sent to your email. The collection is downloading.
RECEIPTRST-{Date.now().toString().slice(-6)}
{item.name}KSh {item.price}
MethodM-PESA · {code} {phone}
PaidKSh {item.price}
);
}
function CheckoutDrawer({ item, onClose, onSuccess }) {
const flow = useMpesaFlow();
return (
<>
§ 04 / Checkout · Drawer
>
);
}
function CheckoutModal({ item, onClose, onSuccess }) {
const flow = useMpesaFlow();
return (
e.stopPropagation()} style={{position:"relative"}}>
§ 04 / Checkout · Modal
);
}
function CheckoutPage({ item, goto, onSuccess }) {
const flow = useMpesaFlow();
return (
sema ikutoke — checkout
§ 04 / Order
Kamau Studio
{item.name}
23 photos · 14 essays · 42 MB
KSh {item.price}
SubtotalKSh {item.price}
M-PESA feeKSh 0
Tax (16% VAT)included
TotalKSh {item.price}
Secured by Safaricom M-PESA Daraja · Settled in 11 seconds
Pay with M-PESA.
M·PESA
M-PESA
STK push to your phone — no card, no fuss
✓
goto("store")} onSuccess={onSuccess} />
);
}
Object.assign(window, {
Builder, Storefront, CheckoutDrawer, CheckoutModal, CheckoutPage,
DEFAULT_PAGE, BLOCK_DEFS,
});