/** * — browse catalog, view orders, trigger fulfillment. * Shows catalog items, order creation flow, and order status tracking. */ import { catalogSchema, catalogDocId, type CatalogDoc, orderSchema, type OrderDoc } from "../schemas"; import type { DocumentId } from "../../../shared/local-first/document"; class FolkCartShop extends HTMLElement { private shadow: ShadowRoot; private space = "default"; private catalog: any[] = []; private orders: any[] = []; private view: "catalog" | "orders" = "catalog"; private loading = true; private _offlineUnsubs: (() => void)[] = []; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { // Resolve space from attribute or URL path const attr = this.getAttribute("space"); if (attr) { this.space = attr; } else { const parts = window.location.pathname.split("/").filter(Boolean); this.space = parts.length >= 1 ? parts[0] : "default"; } if (this.space === "demo") { this.loadDemoData(); return; } this.render(); this.subscribeOffline(); this.loadData(); } disconnectedCallback() { for (const unsub of this._offlineUnsubs) unsub(); this._offlineUnsubs = []; } private async subscribeOffline() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; try { // Subscribe to catalog (single doc per space) const catDocId = catalogDocId(this.space) as DocumentId; const catDoc = await runtime.subscribe(catDocId, catalogSchema); if (catDoc?.items && Object.keys(catDoc.items).length > 0 && this.catalog.length === 0) { this.catalog = Object.values((catDoc as CatalogDoc).items).map(item => ({ id: item.id, title: item.title, description: '', product_type: item.productType, status: item.status, tags: item.tags || [], created_at: new Date(item.createdAt).toISOString(), })); this.loading = false; this.render(); } this._offlineUnsubs.push(runtime.onChange(catDocId, (doc: CatalogDoc) => { if (doc?.items) { this.catalog = Object.values(doc.items).map(item => ({ id: item.id, title: item.title, description: '', product_type: item.productType, status: item.status, tags: item.tags || [], created_at: new Date(item.createdAt).toISOString(), })); this.render(); } })); // Subscribe to orders (multi-doc) const orderDocs = await runtime.subscribeModule('cart', 'orders', orderSchema); if (orderDocs.size > 0 && this.orders.length === 0) { const fromDocs: any[] = []; for (const [docId, doc] of orderDocs) { const d = doc as OrderDoc; if (!d?.order) continue; fromDocs.push({ id: d.order.id, status: d.order.status, quantity: d.order.quantity, total_price: d.order.totalPrice, currency: d.order.currency, created_at: new Date(d.order.createdAt).toISOString(), }); } if (fromDocs.length > 0) { this.orders = fromDocs; this.render(); } } } catch { /* runtime unavailable */ } } private loadDemoData() { const now = Date.now(); this.catalog = [ { id: "demo-cat-1", title: "The Commons", description: "A pocket book exploring shared resources and collective stewardship.", price: 12, currency: "USD", tags: ["books"], product_type: "pocket book", status: "active", created_at: new Date(now - 30 * 86400000).toISOString(), }, { id: "demo-cat-2", title: "Mycelium Networks", description: "Illustrated poster mapping underground fungal communication pathways.", price: 18, currency: "USD", tags: ["prints"], product_type: "poster", status: "active", created_at: new Date(now - 25 * 86400000).toISOString(), }, { id: "demo-cat-3", title: "#DefectFi", description: "Organic cotton tee shirt with the #DefectFi campaign logo.", price: 25, currency: "USD", tags: ["apparel"], product_type: "tee shirt", status: "active", created_at: new Date(now - 20 * 86400000).toISOString(), }, { id: "demo-cat-4", title: "Cosmolocal Sticker Sheet", description: "Die-cut sticker sheet with cosmolocal design motifs.", price: 5, currency: "USD", tags: ["stickers"], product_type: "sticker sheet", status: "active", created_at: new Date(now - 15 * 86400000).toISOString(), }, { id: "demo-cat-5", title: "Doughnut Economics", description: "A zine breaking down Kate Raworth's doughnut economics framework.", price: 8, currency: "USD", tags: ["books"], product_type: "zine", status: "active", created_at: new Date(now - 10 * 86400000).toISOString(), }, { id: "demo-cat-6", title: "rSpace Logo", description: "Embroidered patch featuring the rSpace logo on twill backing.", price: 6, currency: "USD", tags: ["accessories"], product_type: "embroidered patch", status: "active", created_at: new Date(now - 5 * 86400000).toISOString(), }, { id: "demo-cat-7", title: "Cosmolocal Network Tee", description: "Bella+Canvas 3001 tee with the Cosmolocal Network radial design. DTG printed by local providers or Printful.", price: 25, currency: "USD", tags: ["apparel", "cosmolocal"], product_type: "tee", required_capabilities: ["dtg-print"], status: "active", created_at: new Date(now - 3 * 86400000).toISOString(), }, { id: "demo-cat-8", title: "Cosmolocal Sticker Sheet", description: "Kiss-cut vinyl sticker sheet with cosmolocal network motifs. Weatherproof and UV-resistant.", price: 5, currency: "USD", tags: ["stickers", "cosmolocal"], product_type: "sticker-sheet", required_capabilities: ["vinyl-cut"], status: "active", created_at: new Date(now - 1 * 86400000).toISOString(), }, ]; this.orders = [ { id: "demo-ord-1001", items: [ { title: "The Commons", qty: 1, price: 12 }, { title: "Mycelium Networks", qty: 1, price: 18 }, ], total: 30, total_price: "30.00", currency: "USD", status: "paid", created_at: new Date(now - 2 * 86400000).toISOString(), customer_email: "reader@example.com", artifact_title: "Order #1001", quantity: 2, }, { id: "demo-ord-1002", items: [ { title: "#DefectFi", qty: 1, price: 25 }, ], total: 25, total_price: "25.00", currency: "USD", status: "pending", created_at: new Date(now - 1 * 86400000).toISOString(), customer_email: "activist@example.com", artifact_title: "Order #1002", quantity: 1, }, { id: "demo-ord-1003", items: [ { title: "Cosmolocal Sticker Sheet", qty: 1, price: 5 }, { title: "Doughnut Economics", qty: 1, price: 8 }, { title: "rSpace Logo", qty: 1, price: 6 }, ], total: 23, total_price: "23.00", currency: "USD", status: "shipped", created_at: new Date(now - 5 * 86400000).toISOString(), customer_email: "maker@example.com", artifact_title: "Order #1003", quantity: 3, }, ]; this.loading = false; this.render(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^(\/[^/]+)?\/rcart/); return match ? match[0] : "/rcart"; } private async loadData() { this.loading = true; this.render(); try { const [catRes, ordRes] = await Promise.all([ fetch(`${this.getApiBase()}/api/catalog?limit=50`), fetch(`${this.getApiBase()}/api/orders?limit=20`), ]); const catData = await catRes.json(); const ordData = await ordRes.json(); this.catalog = catData.entries || []; this.orders = ordData.orders || []; } catch (e) { console.error("Failed to load cart data:", e); } this.loading = false; this.render(); } private render() { this.shadow.innerHTML = `
Shop
${this.loading ? `
⏳ Loading...
` : this.view === "catalog" ? this.renderCatalog() : this.renderOrders()} `; this.shadow.querySelectorAll(".tab").forEach((el) => { el.addEventListener("click", () => { this.view = ((el as HTMLElement).dataset.view || "catalog") as "catalog" | "orders"; this.render(); }); }); } private renderCatalog(): string { if (this.catalog.length === 0) { return `
No items in the catalog yet. Ingest artifacts from rPubs or Swag Designer to list them here.
`; } return `
${this.catalog.map((entry) => `

${this.esc(entry.title || "Untitled")}

${entry.product_type ? `${this.esc(entry.product_type)}` : ""} ${(entry.required_capabilities || []).map((cap: string) => `${this.esc(cap)}`).join("")} ${(entry.tags || []).map((t: string) => `${this.esc(t)}`).join("")}
${entry.description ? `
${this.esc(entry.description)}
` : ""} ${entry.dimensions ? `
${entry.dimensions.width_mm}x${entry.dimensions.height_mm}mm
` : ""} ${entry.price != null ? `
$${parseFloat(entry.price).toFixed(2)} ${entry.currency || ""}
` : ""}
${entry.status}
`).join("")}
`; } private renderOrders(): string { if (this.orders.length === 0) { return `
No orders yet.
`; } return `
${this.orders.map((order) => `

${this.esc(order.artifact_title || "Order")}

${order.provider_name ? `Provider: ${this.esc(order.provider_name)}` : ""} ${order.quantity > 1 ? ` • Qty: ${order.quantity}` : ""}
${order.status}
$${parseFloat(order.total_price || 0).toFixed(2)}
`).join("")}
`; } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } } customElements.define("folk-cart-shop", FolkCartShop);