/** * — group shopping carts, cosmolocal catalog, and orders. * Three-tab view: Carts (default) | Catalog | Orders */ import { catalogSchema, catalogDocId, type CatalogDoc, orderSchema, type OrderDoc, shoppingCartIndexSchema, shoppingCartIndexDocId, type ShoppingCartIndexDoc, shoppingCartSchema, shoppingCartDocId, type ShoppingCartDoc, } from "../schemas"; import type { DocumentId } from "../../../shared/local-first/document"; import { TourEngine } from "../../../shared/tour-engine"; import { ViewHistory } from "../../../shared/view-history.js"; class FolkCartShop extends HTMLElement { private shadow: ShadowRoot; private space = "default"; private catalog: any[] = []; private orders: any[] = []; private carts: any[] = []; private payments: any[] = []; private groupBuys: any[] = []; private view: "carts" | "cart-detail" | "catalog" | "catalog-detail" | "orders" | "payments" | "group-buys" = "carts"; private selectedCartId: string | null = null; private selectedCart: any = null; private selectedCatalogItem: any = null; private detailQuantity = 1; private orderQueue: any[] = []; private orderQueueOpen = false; private loading = true; private addingUrl = false; private contributingAmount = false; private extensionInstalled = false; private bannerDismissed = false; private creatingPayment = false; private creatingGroupBuy = false; private _offlineUnsubs: (() => void)[] = []; private _history = new ViewHistory<"carts" | "cart-detail" | "catalog" | "catalog-detail" | "orders" | "payments" | "group-buys">("carts"); // Guided tour private _tour!: TourEngine; private static readonly TOUR_STEPS = [ { target: "[data-action='new-cart']", title: "Create a Cart", message: "Start a new group cart — add a name and invite contributors to add items. Use the navigation bar above to switch between Carts, Catalog, Group Buys, Orders, and Payments.", advanceOnClick: true }, ]; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); this._tour = new TourEngine( this.shadow, FolkCartShop.TOUR_STEPS, "rcart_tour_done", () => this.shadow.host as HTMLElement, ); } connectedCallback() { this.extensionInstalled = !!(window as any).__rspaceExtensionInstalled; this.bannerDismissed = localStorage.getItem('rcart-ext-banner-dismissed') === 'true'; 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"; } // Check URL params for initial tab const params = new URLSearchParams(window.location.search); if (params.get('tab') === 'catalog') this.view = 'catalog'; if (params.get('tab') === 'group-buys') this.view = 'group-buys'; if (this.space === "demo") { this.loadDemoData(); } else { this.render(); this.subscribeOffline(); this.loadData(); } // Auto-start tour on first visit if (!localStorage.getItem("rcart_tour_done")) { setTimeout(() => this._tour.start(), 1200); } } 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 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 shopping cart index const indexDocId = shoppingCartIndexDocId(this.space) as DocumentId; const indexDoc = await runtime.subscribe(indexDocId, shoppingCartIndexSchema); if (indexDoc?.carts && Object.keys(indexDoc.carts).length > 0) { this.carts = Object.entries((indexDoc as ShoppingCartIndexDoc).carts).map(([id, c]) => ({ id, ...c })); this.render(); } this._offlineUnsubs.push(runtime.onChange(indexDocId, (doc: ShoppingCartIndexDoc) => { if (doc?.carts) { this.carts = Object.entries(doc.carts).map(([id, c]) => ({ id, ...c })); this.render(); } })); // Subscribe to orders const orderDocs = await runtime.subscribeModule('cart', 'orders', orderSchema); if (orderDocs.size > 0 && this.orders.length === 0) { const fromDocs: any[] = []; for (const [, 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(); // Demo shopping carts this.carts = [ { id: "demo-cart-1", name: "Community Garden Project", status: "FUNDING", itemCount: 8, totalAmount: 247.83, fundedAmount: 165.00, currency: "USD", createdAt: new Date(now - 7 * 86400000).toISOString(), updatedAt: new Date(now - 1 * 86400000).toISOString(), }, { id: "demo-cart-2", name: "Office Supplies", status: "OPEN", itemCount: 3, totalAmount: 45.97, fundedAmount: 0, currency: "USD", createdAt: new Date(now - 2 * 86400000).toISOString(), updatedAt: new Date(now - 2 * 86400000).toISOString(), }, { id: "demo-cart-3", name: "Hackathon Merch Order", status: "ORDERED", itemCount: 5, totalAmount: 189.50, fundedAmount: 189.50, currency: "USD", createdAt: new Date(now - 14 * 86400000).toISOString(), updatedAt: new Date(now - 3 * 86400000).toISOString(), }, ]; // Demo cart detail (Community Garden Project) this.selectedCart = { id: "demo-cart-1", name: "Community Garden Project", description: "Supplies for the community garden build day", status: "FUNDING", targetAmount: 300, fundedAmount: 165.00, currency: "USD", createdAt: new Date(now - 7 * 86400000).toISOString(), updatedAt: new Date(now - 1 * 86400000).toISOString(), items: [ { id: "i1", name: "Raised Garden Bed Kit (4x8 ft)", price: 79.99, currency: "USD", quantity: 1, sourceUrl: "https://amazon.com/dp/B08ABC1234", imageUrl: null, vendor: { name: "Amazon", domain: "amazon.com", platform: "amazon" }, addedAt: new Date(now - 6 * 86400000).toISOString() }, { id: "i2", name: "Organic Potting Soil (40 qt)", price: 12.99, currency: "USD", quantity: 3, sourceUrl: "https://amazon.com/dp/B08DEF5678", imageUrl: null, vendor: { name: "Amazon", domain: "amazon.com", platform: "amazon" }, addedAt: new Date(now - 6 * 86400000).toISOString() }, { id: "i3", name: "Heirloom Seed Variety Pack", price: 24.95, currency: "USD", quantity: 2, sourceUrl: "https://etsy.com/listing/123456", imageUrl: null, vendor: { name: "GardenSeedCo", domain: "etsy.com", platform: "etsy" }, addedAt: new Date(now - 5 * 86400000).toISOString() }, { id: "i4", name: "Garden Trowel Set", price: 15.99, currency: "USD", quantity: 1, sourceUrl: "https://amazon.com/dp/B08GHI9012", imageUrl: null, vendor: { name: "Amazon", domain: "amazon.com", platform: "amazon" }, addedAt: new Date(now - 5 * 86400000).toISOString() }, { id: "i5", name: "Drip Irrigation Kit", price: 29.99, currency: "USD", quantity: 1, sourceUrl: "https://amazon.com/dp/B08JKL3456", imageUrl: null, vendor: { name: "Amazon", domain: "amazon.com", platform: "amazon" }, addedAt: new Date(now - 4 * 86400000).toISOString() }, { id: "i6", name: "Plant Labels (100 pack)", price: 8.99, currency: "USD", quantity: 1, sourceUrl: "https://amazon.com/dp/B08MNO7890", imageUrl: null, vendor: { name: "Amazon", domain: "amazon.com", platform: "amazon" }, addedAt: new Date(now - 4 * 86400000).toISOString() }, { id: "i7", name: "Gardening Gloves (3 pair)", price: 12.99, currency: "USD", quantity: 1, sourceUrl: "https://amazon.com/dp/B08PQR1234", imageUrl: null, vendor: { name: "Amazon", domain: "amazon.com", platform: "amazon" }, addedAt: new Date(now - 3 * 86400000).toISOString() }, { id: "i8", name: "Compost Bin (80 gal)", price: 34.99, currency: "USD", quantity: 1, sourceUrl: "https://homedepot.com/p/12345", imageUrl: null, vendor: { name: "Home Depot", domain: "homedepot.com", platform: null }, addedAt: new Date(now - 2 * 86400000).toISOString() }, ], contributions: [ { id: "c1", username: "Alice", amount: 50, currency: "USD", paymentMethod: "MANUAL", status: "confirmed", createdAt: new Date(now - 5 * 86400000).toISOString() }, { id: "c2", username: "Bob", amount: 40, currency: "USD", paymentMethod: "MANUAL", status: "confirmed", createdAt: new Date(now - 4 * 86400000).toISOString() }, { id: "c3", username: "Carol", amount: 50, currency: "USD", paymentMethod: "MANUAL", status: "confirmed", createdAt: new Date(now - 3 * 86400000).toISOString() }, { id: "c4", username: "Dave", amount: 25, currency: "USD", paymentMethod: "MANUAL", status: "confirmed", createdAt: new Date(now - 1 * 86400000).toISOString() }, ], events: [], }; // Existing catalog demo data 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", image_url: "/images/catalog/catalog-the-commons.jpg", 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", image_url: "/images/catalog/catalog-mycelium-networks.jpg", created_at: new Date(now - 25 * 86400000).toISOString() }, { id: "demo-cat-3", title: "#DefectFi Tee", description: "Organic cotton tee with the #DefectFi campaign logo. Circuit patterns dissolving into organic roots.", price: 25, currency: "USD", tags: ["apparel"], product_type: "tee shirt", status: "active", image_url: "/images/catalog/catalog-defectfi-tee.jpg", created_at: new Date(now - 20 * 86400000).toISOString() }, { id: "demo-cat-4", title: "Cosmolocal Sticker Sheet", description: "Die-cut sticker set with cosmolocal motifs — nodes, mycelium, community gardens, mesh networks.", price: 5, currency: "USD", tags: ["stickers"], product_type: "sticker sheet", status: "active", image_url: "/images/catalog/catalog-cosmolocal-stickers.jpg", created_at: new Date(now - 15 * 86400000).toISOString() }, { id: "demo-cat-5", title: "Doughnut Economics Zine", description: "Punk zine breaking down Kate Raworth's doughnut economics framework. Cut-and-paste collage aesthetic.", price: 8, currency: "USD", tags: ["books", "zines"], product_type: "zine", status: "active", image_url: "/images/catalog/catalog-doughnut-economics.jpg", created_at: new Date(now - 10 * 86400000).toISOString() }, { id: "demo-cat-6", title: "rSpace Logo Patch", description: "Embroidered patch featuring the rSpace logo in teal and white on navy twill backing.", price: 6, currency: "USD", tags: ["accessories"], product_type: "embroidered patch", status: "active", image_url: "/images/catalog/catalog-rspace-patch.jpg", 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 in teal and coral.", price: 25, currency: "USD", tags: ["apparel", "cosmolocal"], product_type: "tee", required_capabilities: ["dtg-print"], status: "active", image_url: "/images/catalog/catalog-cosmolocal-tee.jpg", created_at: new Date(now - 3 * 86400000).toISOString() }, { id: "demo-cat-8", title: "Cosmolocal Vinyl Stickers", description: "Kiss-cut vinyl sticker sheet with network constellation patterns and holographic accents.", price: 5, currency: "USD", tags: ["stickers", "cosmolocal"], product_type: "sticker-sheet", required_capabilities: ["vinyl-cut"], status: "active", image_url: "/images/catalog/catalog-cosmolocal-vinyl-stickers.jpg", created_at: new Date(now - 1 * 86400000).toISOString() }, ]; this.orders = [ { id: "demo-ord-1001", total_price: "30.00", currency: "USD", status: "paid", created_at: new Date(now - 2 * 86400000).toISOString(), artifact_title: "Order #1001", quantity: 2 }, { id: "demo-ord-1002", total_price: "25.00", currency: "USD", status: "pending", created_at: new Date(now - 1 * 86400000).toISOString(), artifact_title: "Order #1002", quantity: 1 }, { id: "demo-ord-1003", total_price: "23.00", currency: "USD", status: "shipped", created_at: new Date(now - 5 * 86400000).toISOString(), artifact_title: "Order #1003", quantity: 3 }, ]; this.payments = [ { id: "demo-pay-1", description: "Coffee tip", amount: "5.00", token: "USDC", chainId: 8453, recipientAddress: "0x1234...abcd", status: "paid", paymentMethod: "wallet", txHash: "0xabc123...", created_at: new Date(now - 1 * 86400000).toISOString(), paid_at: new Date(now - 1 * 86400000).toISOString() }, { id: "demo-pay-2", description: "Invoice #42", amount: "25.00", token: "USDC", chainId: 8453, recipientAddress: "0x1234...abcd", status: "pending", paymentMethod: null, txHash: null, created_at: new Date(now - 3600000).toISOString(), paid_at: null }, ]; this.groupBuys = [ { id: "demo-gb-1", title: "Cosmolocal Network Tee", productType: "tee", imageUrl: "/images/catalog/catalog-cosmolocal-tee.jpg", status: "OPEN", totalPledged: 17, tiers: [ { min_qty: 1, per_unit: 25, currency: "USD" }, { min_qty: 10, per_unit: 21.25, currency: "USD" }, { min_qty: 25, per_unit: 18, currency: "USD" }, { min_qty: 50, per_unit: 15, currency: "USD" }, ], closesAt: new Date(now + 14 * 86400000).toISOString(), createdAt: new Date(now - 5 * 86400000).toISOString(), }, { id: "demo-gb-2", title: "#DefectFi Tee", productType: "tee shirt", imageUrl: "/images/catalog/catalog-defectfi-tee.jpg", status: "OPEN", totalPledged: 8, tiers: [ { min_qty: 1, per_unit: 25, currency: "USD" }, { min_qty: 10, per_unit: 21.25, currency: "USD" }, { min_qty: 25, per_unit: 18, currency: "USD" }, { min_qty: 50, per_unit: 15, currency: "USD" }, ], closesAt: new Date(now + 21 * 86400000).toISOString(), createdAt: new Date(now - 2 * 86400000).toISOString(), }, { id: "demo-gb-3", title: "Doughnut Economics Zine", productType: "zine", imageUrl: "/images/catalog/catalog-doughnut-economics.jpg", status: "OPEN", totalPledged: 32, tiers: [ { min_qty: 1, per_unit: 8, currency: "USD" }, { min_qty: 15, per_unit: 6.80, currency: "USD" }, { min_qty: 30, per_unit: 5.60, currency: "USD" }, { min_qty: 60, per_unit: 4.80, currency: "USD" }, ], closesAt: new Date(now + 10 * 86400000).toISOString(), createdAt: new Date(now - 8 * 86400000).toISOString(), }, ]; 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, cartRes] = await Promise.all([ fetch(`${this.getApiBase()}/api/catalog?limit=50`), fetch(`${this.getApiBase()}/api/orders?limit=20`), fetch(`${this.getApiBase()}/api/shopping-carts`), ]); const catData = await catRes.json(); const ordData = await ordRes.json(); const cartData = await cartRes.json(); this.catalog = catData.entries || []; this.orders = ordData.orders || []; this.carts = cartData.carts || []; // Load payments (auth-gated, may fail for unauthenticated users) try { const payRes = await fetch(`${this.getApiBase()}/api/payments`); if (payRes.ok) { const payData = await payRes.json(); this.payments = payData.payments || []; } } catch { /* unauthenticated */ } // Load group buys try { const gbRes = await fetch(`${this.getApiBase()}/api/group-buys`); if (gbRes.ok) { const gbData = await gbRes.json(); this.groupBuys = gbData.groupBuys || []; } } catch { /* silent */ } } catch (e) { console.error("Failed to load cart data:", e); } this.loading = false; this.render(); } private async loadCartDetail(cartId: string) { if (this.space === "demo") { this.selectedCartId = cartId; this.view = "cart-detail"; this.render(); return; } try { const res = await fetch(`${this.getApiBase()}/api/shopping-carts/${cartId}`); this.selectedCart = await res.json(); this.selectedCartId = cartId; this.view = "cart-detail"; } catch (e) { console.error("Failed to load cart:", e); } this.render(); } private async createCart(name: string, targetAmount: number) { if (this.space === "demo") return; try { await fetch(`${this.getApiBase()}/api/shopping-carts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, targetAmount }), }); await this.loadData(); } catch (e) { console.error("Failed to create cart:", e); } } private async addItemByUrl(cartId: string, url: string) { this.addingUrl = true; this.render(); try { await fetch(`${this.getApiBase()}/api/shopping-carts/${cartId}/items`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); await this.loadCartDetail(cartId); } catch (e) { console.error("Failed to add item:", e); } this.addingUrl = false; } private async removeItem(cartId: string, itemId: string) { try { await fetch(`${this.getApiBase()}/api/shopping-carts/${cartId}/items/${itemId}`, { method: 'DELETE' }); await this.loadCartDetail(cartId); } catch (e) { console.error("Failed to remove item:", e); } } private async contribute(cartId: string, amount: number, username: string) { try { await fetch(`${this.getApiBase()}/api/shopping-carts/${cartId}/contribute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount, username }), }); await this.loadCartDetail(cartId); } catch (e) { console.error("Failed to contribute:", e); } } // ── Main render ── private render() { const styles = this.getStyles(); let content = ''; if (this.loading) { content = `
Loading...
`; } else if (this.view === "carts") { content = this.renderCarts(); } else if (this.view === "cart-detail") { content = this.renderCartDetail(); } else if (this.view === "catalog") { content = this.renderCatalog(); } else if (this.view === "catalog-detail") { content = this.renderCatalogDetail(); } else if (this.view === "payments") { content = this.renderPayments(); } else if (this.view === "group-buys") { content = this.renderGroupBuys(); } else { content = this.renderOrders(); } this.shadow.innerHTML = `
Shop
${this.orderQueue.length > 0 ? `` : ''}
${this.orderQueueOpen && this.orderQueue.length > 0 ? this.renderOrderQueue() : ''} ${content} `; this.bindEvents(); this._tour.renderOverlay(); } startTour() { this._tour.start(); } private goBack() { const prev = this._history.back(); if (!prev) return; this.view = prev.view; if (prev.view !== "cart-detail") this.selectedCartId = null; this.render(); } private bindEvents() { // Tour button this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour()); // Extension banner dismiss this.shadow.querySelector("[data-action='dismiss-banner']")?.addEventListener("click", () => { this.bannerDismissed = true; localStorage.setItem('rcart-ext-banner-dismissed', 'true'); this.render(); }); // Cart card clicks this.shadow.querySelectorAll("[data-cart-id]").forEach((el) => { el.addEventListener("click", () => { this._history.push(this.view); this._history.push("cart-detail", { cartId: (el as HTMLElement).dataset.cartId }); this.loadCartDetail((el as HTMLElement).dataset.cartId!); }); }); // Back button this.shadow.querySelector("[data-action='back']")?.addEventListener("click", () => { this.goBack(); }); // New cart form const newCartBtn = this.shadow.querySelector("[data-action='new-cart']"); newCartBtn?.addEventListener("click", () => { const form = this.shadow.querySelector(".new-cart-form") as HTMLElement; if (form) form.style.display = form.style.display === 'none' ? 'flex' : 'none'; }); this.shadow.querySelector("[data-action='create-cart']")?.addEventListener("click", () => { const nameInput = this.shadow.querySelector("[data-field='cart-name']") as HTMLInputElement; const amountInput = this.shadow.querySelector("[data-field='cart-target']") as HTMLInputElement; if (nameInput?.value) this.createCart(nameInput.value, parseFloat(amountInput?.value) || 0); }); // Add item by URL this.shadow.querySelector("[data-action='add-url']")?.addEventListener("click", () => { const input = this.shadow.querySelector("[data-field='item-url']") as HTMLInputElement; if (input?.value && this.selectedCartId) { this.addItemByUrl(this.selectedCartId, input.value); } }); // Remove item this.shadow.querySelectorAll("[data-remove-item]").forEach((el) => { el.addEventListener("click", (e) => { e.stopPropagation(); if (this.selectedCartId) this.removeItem(this.selectedCartId, (el as HTMLElement).dataset.removeItem!); }); }); // Contribute toggle this.shadow.querySelector("[data-action='show-contribute']")?.addEventListener("click", () => { const form = this.shadow.querySelector(".contribute-form") as HTMLElement; if (form) form.style.display = form.style.display === 'none' ? 'flex' : 'none'; }); this.shadow.querySelector("[data-action='submit-contribute']")?.addEventListener("click", () => { const amtInput = this.shadow.querySelector("[data-field='contrib-amount']") as HTMLInputElement; const nameInput = this.shadow.querySelector("[data-field='contrib-name']") as HTMLInputElement; if (amtInput?.value && this.selectedCartId) { this.contribute(this.selectedCartId, parseFloat(amtInput.value), nameInput?.value || 'Anonymous'); } }); // Payment request actions const newPaymentBtn = this.shadow.querySelector("[data-action='new-payment']"); newPaymentBtn?.addEventListener("click", () => { const form = this.shadow.querySelector(".new-payment-form") as HTMLElement; if (form) form.style.display = form.style.display === 'none' ? 'flex' : 'none'; }); this.shadow.querySelector("[data-action='create-payment']")?.addEventListener("click", () => { this.createPaymentRequest(); }); this.shadow.querySelectorAll("[data-action='copy-pay-url']").forEach((el) => { el.addEventListener("click", () => { const payId = (el as HTMLElement).dataset.payId; const url = `${window.location.origin}/${this.space}/rcart/pay/${payId}`; navigator.clipboard.writeText(url); (el as HTMLElement).textContent = 'Copied!'; setTimeout(() => { (el as HTMLElement).textContent = 'Copy Link'; }, 2000); }); }); // Group buy card clicks → navigate to group buy page this.shadow.querySelectorAll("[data-group-buy-id]").forEach((el) => { el.addEventListener("click", () => { const buyId = (el as HTMLElement).dataset.groupBuyId!; const url = `/${this.space}/rcart/buy/${buyId}`; window.location.href = url; }); }); // Catalog card clicks → detail view this.shadow.querySelectorAll("[data-catalog-id]").forEach((el) => { el.addEventListener("click", () => { this.loadCatalogDetail((el as HTMLElement).dataset.catalogId!); }); }); // Catalog detail quantity controls this.shadow.querySelector("[data-action='qty-dec']")?.addEventListener("click", () => { if (this.detailQuantity > 1) { this.detailQuantity--; this.render(); } }); this.shadow.querySelector("[data-action='qty-inc']")?.addEventListener("click", () => { this.detailQuantity++; this.render(); }); this.shadow.querySelector("[data-field='detail-qty']")?.addEventListener("change", (e) => { const val = parseInt((e.target as HTMLInputElement).value) || 1; this.detailQuantity = Math.max(1, val); this.render(); }); // Add to queue this.shadow.querySelector("[data-action='add-to-queue']")?.addEventListener("click", () => { this.addToQueue(); }); // Start group buy this.shadow.querySelector("[data-action='start-group-buy']")?.addEventListener("click", () => { this.startGroupBuy(); }); // Order queue toggle this.shadow.querySelectorAll("[data-action='toggle-queue']").forEach((el) => { el.addEventListener("click", () => { this.orderQueueOpen = !this.orderQueueOpen; this.render(); }); }); // Remove from queue this.shadow.querySelectorAll("[data-remove-queue]").forEach((el) => { el.addEventListener("click", () => { const idx = parseInt((el as HTMLElement).dataset.removeQueue!); this.orderQueue.splice(idx, 1); if (this.orderQueue.length === 0) this.orderQueueOpen = false; this.render(); }); }); } // ── Extension install banner ── private renderExtensionBanner(): string { if (this.extensionInstalled || this.bannerDismissed) return ''; return `
Add products from any store
Install the rSpace browser extension to add items from Amazon, Etsy, Shopify, and more directly to your carts.
Get Extension
`; } // ── Cart list view ── private renderCarts(): string { const newCartForm = ` `; if (this.carts.length === 0) { return ` ${this.renderExtensionBanner()}

No shopping carts yet. Create one to start group shopping.

${newCartForm}
`; } return ` ${this.renderExtensionBanner()}
${newCartForm}
${this.carts.map((cart) => { const pct = cart.totalAmount > 0 ? Math.min(100, Math.round((cart.fundedAmount / cart.totalAmount) * 100)) : 0; return `

${this.esc(cart.name)}

${cart.status}
${cart.itemCount} item${cart.itemCount !== 1 ? 's' : ''} • $${cart.totalAmount.toFixed(2)}
$${cart.fundedAmount.toFixed(2)} / $${cart.totalAmount.toFixed(2)} funded (${pct}%)
`; }).join("")}
`; } // ── Cart detail view ── private renderCartDetail(): string { const cart = this.selectedCart; if (!cart) return `
Loading cart...
`; const items = cart.items || []; const contributions = cart.contributions || []; const totalItemsCost = items.reduce((s: number, i: any) => s + (i.price || 0) * (i.quantity || 1), 0); const targetDisplay = cart.targetAmount > 0 ? cart.targetAmount : totalItemsCost; const pct = targetDisplay > 0 ? Math.min(100, Math.round((cart.fundedAmount / targetDisplay) * 100)) : 0; const remaining = Math.max(0, targetDisplay - cart.fundedAmount); // Group items by vendor domain const vendorGroups: Record = {}; for (const item of items) { const key = item.vendor?.domain || 'other'; if (!vendorGroups[key]) vendorGroups[key] = []; vendorGroups[key].push(item); } const itemsHtml = Object.entries(vendorGroups).map(([domain, vendorItems]) => `
${this.esc(domain)}
${vendorItems.map((item: any) => `
${item.imageUrl ? `` : `
📦
`}
${this.esc(item.name)}
Qty: ${item.quantity}${item.price != null ? ` • $${(item.price * item.quantity).toFixed(2)}` : ''}
`).join("")}
`).join(""); const contribHtml = contributions.length > 0 ? contributions.map((c: any) => `
${this.esc(c.username)} $${c.amount.toFixed(2)} ${c.status}
`).join("") : `
No contributions yet.
`; return ` ${this._history.canGoBack ? '
' : ''}

${this.esc(cart.name)}

${cart.description ? `

${this.esc(cart.description)}

` : ''}
${items.length === 0 ? `
No items yet. Paste a URL above to add products.
` : itemsHtml}

Summary

Items cost$${totalItemsCost.toFixed(2)}
Funded$${cart.fundedAmount.toFixed(2)}
Remaining$${remaining.toFixed(2)}
${pct}% funded

Contributions

${contribHtml}
`; } // ── Catalog view ── 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) => `
${entry.image_url ? `
${this.esc(entry.title || '')}
` : ""}

${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
` : ""}
`).join("")}
`; } // ── Catalog detail view ── private loadCatalogDetail(id: string) { const item = this.catalog.find((e: any) => e.id === id); if (!item) return; this.selectedCatalogItem = item; this.detailQuantity = 1; this._history.push(this.view); this.view = "catalog-detail"; this._history.push("catalog-detail"); this.render(); } private buildDemoFulfillOptions(entry: any) { const basePrice = entry.price || 10; return { provider: { name: "Community Print Co-op", city: "Portland, OR", turnaround: "5-7 business days", capabilities: entry.required_capabilities || ["dtg-print"], }, tiers: [ { min_qty: 1, per_unit: basePrice, currency: entry.currency || "USD" }, { min_qty: 10, per_unit: +(basePrice * 0.85).toFixed(2), currency: entry.currency || "USD" }, { min_qty: 25, per_unit: +(basePrice * 0.72).toFixed(2), currency: entry.currency || "USD" }, { min_qty: 50, per_unit: +(basePrice * 0.60).toFixed(2), currency: entry.currency || "USD" }, ], }; } private getCurrentTierPrice(): number { if (!this.selectedCatalogItem) return 0; const opts = this.buildDemoFulfillOptions(this.selectedCatalogItem); let price = opts.tiers[0].per_unit; for (const tier of opts.tiers) { if (this.detailQuantity >= tier.min_qty) price = tier.per_unit; } return price; } private renderCatalogDetail(): string { const item = this.selectedCatalogItem; if (!item) return `
Item not found.
`; const opts = this.buildDemoFulfillOptions(item); const unitPrice = this.getCurrentTierPrice(); const totalPrice = (unitPrice * this.detailQuantity).toFixed(2); const basePrice = opts.tiers[0].per_unit; return `
${item.image_url ? `${this.esc(item.title)}` : `
No image
`}

${this.esc(item.title)}

${item.product_type ? `${this.esc(item.product_type)}` : ''} ${(item.tags || []).map((t: string) => `${this.esc(t)}`).join(' ')} ${item.description ? `

${this.esc(item.description)}

` : ''}
Volume Pricing
${opts.tiers.map((t: any, i: number) => { const active = this.detailQuantity >= t.min_qty && (i === opts.tiers.length - 1 || this.detailQuantity < opts.tiers[i + 1].min_qty); const savings = i > 0 ? Math.round((1 - t.per_unit / basePrice) * 100) : 0; return `
${t.min_qty}+ $${t.per_unit.toFixed(2)}/ea ${savings > 0 ? `-${savings}%` : ``}
`; }).join('')}
Total $${totalPrice} ${item.currency || 'USD'}
${this.esc(opts.provider.name)}
${this.esc(opts.provider.city)} • ${this.esc(opts.provider.turnaround)}
`; } // ── Order queue drawer ── private renderOrderQueue(): string { const total = this.orderQueue.reduce((s: number, i: any) => s + i.unitPrice * i.quantity, 0); return `

Order Queue (${this.orderQueue.length})

${this.orderQueue.map((item: any, idx: number) => `
${this.esc(item.title)} Qty: ${item.quantity} • $${(item.unitPrice * item.quantity).toFixed(2)}
`).join('')}
Total $${total.toFixed(2)}
`; } private addToQueue() { if (!this.selectedCatalogItem) return; const item = this.selectedCatalogItem; const unitPrice = this.getCurrentTierPrice(); const existing = this.orderQueue.find((q: any) => q.catalogId === item.id); if (existing) { existing.quantity += this.detailQuantity; existing.unitPrice = unitPrice; } else { this.orderQueue.push({ catalogId: item.id, title: item.title, productType: item.product_type, imageUrl: item.image_url, quantity: this.detailQuantity, unitPrice, currency: item.currency || 'USD', }); } this.render(); } private async startGroupBuy() { if (!this.selectedCatalogItem || this.creatingGroupBuy) return; const item = this.selectedCatalogItem; const opts = this.buildDemoFulfillOptions(item); if (this.space === 'demo') { const demoId = `demo-gb-${Date.now()}`; const host = window.location.host; const shareUrl = `https://${host}/demo/rcart/buy/${demoId}`; try { await navigator.clipboard.writeText(shareUrl); } catch {} alert(`Group buy created! Link copied.\nView all group buys in the Group Buys tab.`); this._history.push(this.view); this.view = 'group-buys'; this._history.push('group-buys'); this.render(); return; } this.creatingGroupBuy = true; this.render(); try { const res = await fetch(`${this.getApiBase()}/api/group-buys`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(localStorage.getItem('encryptid-token') ? { 'Authorization': `Bearer ${localStorage.getItem('encryptid-token')}` } : {}), }, body: JSON.stringify({ catalogEntryId: item.id, tiers: opts.tiers, description: item.description || '', }), }); if (res.ok) { const data = await res.json(); try { await navigator.clipboard.writeText(data.shareUrl); } catch {} // Refresh group buys list and navigate to tab try { const gbRes = await fetch(`${this.getApiBase()}/api/group-buys`); if (gbRes.ok) { const gbData = await gbRes.json(); this.groupBuys = gbData.groupBuys || []; } } catch { /* silent */ } this._history.push(this.view); this.view = 'group-buys'; this._history.push('group-buys'); } } catch (e) { console.error("Failed to create group buy:", e); } this.creatingGroupBuy = false; this.render(); } // ── Group Buys view ── private renderGroupBuys(): string { const open = this.groupBuys.filter((g: any) => g.status === 'OPEN'); const closed = this.groupBuys.filter((g: any) => g.status !== 'OPEN'); if (this.groupBuys.length === 0) { return `

No group buys yet. Start one from any catalog item to unlock volume pricing together.

Browse Catalog
`; } const renderCard = (gb: any) => { const currentTier = [...gb.tiers].reverse().find((t: any) => gb.totalPledged >= t.min_qty) || gb.tiers[0]; const nextTier = gb.tiers.find((t: any) => t.min_qty > gb.totalPledged); const progressPct = nextTier ? Math.min(100, Math.round((gb.totalPledged / nextTier.min_qty) * 100)) : 100; const remaining = nextTier ? nextTier.min_qty - gb.totalPledged : 0; const closesDate = new Date(gb.closesAt); const daysLeft = Math.max(0, Math.ceil((closesDate.getTime() - Date.now()) / 86400000)); const bestTier = gb.tiers[gb.tiers.length - 1]; const savings = currentTier && gb.tiers[0] ? Math.round((1 - currentTier.per_unit / gb.tiers[0].per_unit) * 100) : 0; return `
${gb.imageUrl ? `${this.esc(gb.title)}` : `
🤝
`}

${this.esc(gb.title)}

${gb.productType ? `${this.esc(gb.productType)}` : ''} ${gb.status}
${gb.totalPledged} pledged ${daysLeft}d left $${currentTier.per_unit.toFixed(2)}/ea${savings > 0 ? ` (-${savings}%)` : ''}
${nextTier ? `${remaining} more to unlock $${nextTier.per_unit.toFixed(2)}/ea` : `Best tier unlocked — $${bestTier.per_unit.toFixed(2)}/ea`}
${gb.tiers.map((t: any, i: number) => { const reached = gb.totalPledged >= t.min_qty; const isCurrent = currentTier && t.min_qty === currentTier.min_qty; const tierSavings = i > 0 ? Math.round((1 - t.per_unit / gb.tiers[0].per_unit) * 100) : 0; return `${t.min_qty}+ $${t.per_unit.toFixed(2)}${tierSavings > 0 ? ` (-${tierSavings}%)` : ''}`; }).join('')}
`; }; return ` ${open.length > 0 ? `

Open Group Buys

${open.map(renderCard).join('')}
` : ''} ${closed.length > 0 ? `

Past Group Buys

${closed.map(renderCard).join('')}
` : ''} `; } // ── Orders view ── 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("")}
`; } // ── Payments view ── private renderPayments(): string { const newPaymentForm = ` `; if (this.payments.length === 0) { return `

No payment requests yet. Create one to generate a shareable QR code.

${newPaymentForm}
`; } const chainNames: Record = { 8453: 'Base', 84532: 'Base Sepolia', 1: 'Ethereum' }; return `
${newPaymentForm}
${this.payments.map((pay) => `

${this.esc(pay.description)}

${pay.status}
${this.esc(pay.amount)} ${this.esc(pay.token)}
${chainNames[pay.chainId] || 'Chain ' + pay.chainId}${pay.paymentMethod ? ' • via ' + pay.paymentMethod : ''}
${new Date(pay.created_at).toLocaleDateString()}
${pay.status === 'pending' ? `
Open QR
` : ''} ${pay.txHash ? `
Tx: ${pay.txHash.slice(0, 10)}...${pay.txHash.slice(-6)}
` : ''}
`).join("")}
`; } private async createPaymentRequest() { const desc = (this.shadow.querySelector('[data-field="pay-desc"]') as HTMLInputElement)?.value; const amount = (this.shadow.querySelector('[data-field="pay-amount"]') as HTMLInputElement)?.value; const token = (this.shadow.querySelector('[data-field="pay-token"]') as HTMLSelectElement)?.value || 'USDC'; const recipient = (this.shadow.querySelector('[data-field="pay-recipient"]') as HTMLInputElement)?.value; if (!desc || !amount || !recipient) return; this.creatingPayment = true; this.render(); try { await fetch(`${this.getApiBase()}/api/payments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description: desc, amount, token, recipientAddress: recipient, chainId: 8453, }), }); await this.loadData(); } catch (e) { console.error("Failed to create payment request:", e); } this.creatingPayment = false; } // ── Styles ── private getStyles(): string { return ` :host { display: block; padding: 1.5rem; } * { box-sizing: border-box; } .rapp-nav { display: flex; gap: 8px; align-items: center; margin-bottom: 1rem; min-height: 36px; flex-wrap: wrap; } .rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); } .tabs { display: flex; gap: 0.5rem; } .tab { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.875rem; } .tab:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); } .tab.active { background: var(--rs-primary-hover); border-color: var(--rs-primary); color: #fff; } .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } .card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.25rem; } .card:hover { border-color: var(--rs-border-strong); } .card-clickable { cursor: pointer; transition: border-color 0.15s, transform 0.1s; } .card-clickable:hover { border-color: var(--rs-primary); transform: translateY(-1px); } .card-title { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; margin: 0 0 0.5rem; } .card-meta { color: var(--rs-text-secondary); font-size: 0.8125rem; margin-bottom: 0.5rem; } .section-title { color: var(--rs-text-primary); font-weight: 700; font-size: 1.25rem; margin: 0 0 0.5rem; } .tag { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.6875rem; margin-right: 0.25rem; } .tag-type { background: rgba(99,102,241,0.1); color: var(--rs-primary-hover); } .tag-cap { background: rgba(34,197,94,0.1); color: #4ade80; } .dims { color: var(--rs-text-muted); font-size: 0.75rem; margin-top: 0.5rem; } .status { padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.6875rem; font-weight: 500; white-space: nowrap; } .status-pending, .status-open { background: rgba(251,191,36,0.15); color: #fbbf24; } .status-paid, .status-active, .status-confirmed, .status-funded { background: rgba(34,197,94,0.15); color: #4ade80; } .status-funding { background: rgba(99,102,241,0.15); color: #a5b4fc; } .status-completed, .status-ordered, .status-checking_out { background: rgba(99,102,241,0.15); color: #a5b4fc; } .status-cancelled, .status-closed { background: rgba(239,68,68,0.15); color: #f87171; } .status-shipped { background: rgba(56,189,248,0.15); color: #38bdf8; } .price { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; } /* Catalog product cards */ .catalog-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); } .catalog-card { padding: 0; overflow: hidden; display: flex; flex-direction: column; } .catalog-img { width: 100%; aspect-ratio: 1; overflow: hidden; background: var(--rs-bg-surface-raised); } .catalog-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s; } .catalog-card:hover .catalog-img img { transform: scale(1.04); } .catalog-body { padding: 1rem; display: flex; flex-direction: column; flex: 1; } .catalog-body .card-title { margin-bottom: 0.25rem; } .card-desc { color: var(--rs-text-secondary); font-size: 0.8125rem; margin-bottom: 0.75rem; line-height: 1.4; flex: 1; } .catalog-footer { display: flex; align-items: center; justify-content: space-between; margin-top: auto; } .text-green { color: #4ade80; } .progress-bar { background: var(--rs-bg-surface-raised); border-radius: 999px; height: 8px; overflow: hidden; } .progress-fill { background: linear-gradient(90deg, var(--rs-primary), #8b5cf6); height: 100%; border-radius: 999px; transition: width 0.3s; } .btn { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); cursor: pointer; font-size: 0.875rem; } .btn:hover { border-color: var(--rs-border-strong); } .btn-primary { background: var(--rs-primary-hover); border-color: var(--rs-primary); color: #fff; } .btn-primary:hover { background: #4338ca; } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; } .btn-icon { background: none; border: none; color: var(--rs-text-muted); cursor: pointer; padding: 4px; font-size: 0.875rem; border-radius: 4px; } .btn-icon:hover { color: #f87171; background: rgba(239,68,68,0.1); } .input { padding: 0.5rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; } .input:focus { outline: none; border-color: var(--rs-primary); } .input-sm { max-width: 160px; } .new-cart-form, .contribute-form, .new-payment-form { display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem; flex-wrap: wrap; } .url-input-row { display: flex; gap: 0.5rem; margin-bottom: 1rem; } .detail-layout { display: grid; grid-template-columns: 1fr 320px; gap: 1.5rem; } @media (max-width: 768px) { .detail-layout { grid-template-columns: 1fr; } } .vendor-group { margin-bottom: 1rem; } .vendor-header { color: var(--rs-text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.5rem 0; border-bottom: 1px solid var(--rs-border-subtle); margin-bottom: 0.5rem; } .item-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--rs-border-subtle); } .item-thumb { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; background: var(--rs-bg-surface-raised); flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; } .item-thumb-placeholder { width: 40px; height: 40px; border-radius: 6px; background: var(--rs-bg-surface-raised); display: flex; align-items: center; justify-content: center; font-size: 1.25rem; } .item-info { flex: 1; min-width: 0; } .item-name { color: var(--rs-text-primary); font-size: 0.875rem; text-decoration: none; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-name:hover { color: var(--rs-primary-hover); } .item-meta { color: var(--rs-text-muted); font-size: 0.75rem; } .summary-row { display: flex; justify-content: space-between; padding: 0.375rem 0; color: var(--rs-text-primary); font-size: 0.875rem; } .contrib-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0; font-size: 0.8125rem; } .contrib-name { color: var(--rs-text-primary); flex: 1; } .contrib-amount { color: #4ade80; font-weight: 600; } .order-card { display: flex; justify-content: space-between; align-items: center; } .order-info { flex: 1; } .order-price { color: var(--rs-text-primary); font-weight: 600; font-size: 1.125rem; } .ext-banner { position: relative; background: rgba(99,102,241,0.08); border: 1px solid rgba(99,102,241,0.25); border-radius: 12px; padding: 1rem 2.5rem 1rem 1.25rem; margin-bottom: 1rem; } .ext-banner-dismiss { position: absolute; top: 0.5rem; right: 0.75rem; background: none; border: none; color: var(--rs-text-secondary); font-size: 1.25rem; cursor: pointer; padding: 0 4px; line-height: 1; } .ext-banner-dismiss:hover { color: var(--rs-text-primary); } .ext-banner-title { color: #a5b4fc; font-weight: 600; font-size: 0.9375rem; margin-bottom: 0.25rem; } .ext-banner-text { color: var(--rs-text-secondary); font-size: 0.8125rem; margin-bottom: 0.75rem; line-height: 1.4; } .ext-banner-cta { display: inline-block; padding: 0.375rem 1rem; border-radius: 6px; background: var(--rs-primary-hover); color: #fff; font-size: 0.8125rem; font-weight: 600; text-decoration: none; } .ext-banner-cta:hover { background: #4338ca; } .empty { text-align: center; padding: 3rem; color: var(--rs-text-muted); font-size: 0.875rem; } .loading { text-align: center; padding: 3rem; color: var(--rs-text-secondary); } /* Catalog detail view */ .detail-back { margin-bottom: 1rem; } .catalog-detail-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; } .detail-image img { width: 100%; border-radius: 12px; aspect-ratio: 1; object-fit: cover; } .detail-image-placeholder { width: 100%; aspect-ratio: 1; background: var(--rs-bg-surface-raised); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: var(--rs-text-muted); } .detail-title { color: var(--rs-text-primary); font-size: 1.5rem; font-weight: 700; margin: 0 0 0.5rem; } .detail-desc { color: var(--rs-text-secondary); font-size: 0.875rem; line-height: 1.5; margin: 0.75rem 0; } .tier-table { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; overflow: hidden; margin: 1rem 0; } .tier-header { padding: 0.625rem 1rem; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--rs-text-secondary); border-bottom: 1px solid var(--rs-border); } .tier-row { display: flex; align-items: center; padding: 0.5rem 1rem; border-bottom: 1px solid var(--rs-border-subtle); gap: 1rem; transition: background 0.15s; } .tier-row:last-child { border-bottom: none; } .tier-active { background: rgba(99,102,241,0.08); } .tier-qty { color: var(--rs-text-primary); font-weight: 600; min-width: 3rem; } .tier-price { color: var(--rs-text-primary); flex: 1; } .tier-savings { color: #4ade80; font-size: 0.8125rem; font-weight: 500; min-width: 3rem; text-align: right; } .qty-row { display: flex; align-items: center; justify-content: space-between; margin: 1rem 0; } .qty-row label { color: var(--rs-text-primary); font-weight: 600; } .qty-controls { display: flex; align-items: center; gap: 0.5rem; } .qty-input { width: 60px; text-align: center; padding: 0.375rem; border-radius: 8px; border: 1px solid var(--rs-input-border); background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.875rem; } .detail-total { display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-top: 1px solid var(--rs-border); border-bottom: 1px solid var(--rs-border); } .detail-total span:first-child { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; } .provider-info { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; padding: 0.75rem 1rem; margin: 1rem 0; } .provider-name { color: var(--rs-text-primary); font-weight: 600; font-size: 0.875rem; } .provider-meta { color: var(--rs-text-muted); font-size: 0.8125rem; margin-top: 0.25rem; } .detail-actions { display: flex; gap: 0.75rem; margin-top: 1rem; } .detail-actions .btn { flex: 1; text-align: center; } /* Order queue */ .queue-badge { background: var(--rs-primary-hover); color: #fff; border: none; border-radius: 999px; padding: 0.375rem 0.875rem; font-size: 0.8125rem; font-weight: 600; cursor: pointer; } .queue-badge:hover { background: #4338ca; } .queue-drawer { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1rem; margin-bottom: 1rem; } .queue-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } .queue-header h3 { margin: 0; color: var(--rs-text-primary); font-size: 1rem; } .queue-item { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid var(--rs-border-subtle); } .queue-item:last-of-type { border-bottom: none; } .queue-item-info { flex: 1; } .queue-item-title { color: var(--rs-text-primary); font-size: 0.875rem; display: block; } .queue-item-meta { color: var(--rs-text-muted); font-size: 0.75rem; } .queue-total { display: flex; justify-content: space-between; padding-top: 0.75rem; margin-top: 0.5rem; border-top: 1px solid var(--rs-border); } /* Group buy cards */ .gb-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); } .gb-card { padding: 1rem; } .gb-card-top { display: flex; gap: 0.75rem; margin-bottom: 0.75rem; } .gb-card-img { width: 64px; height: 64px; border-radius: 10px; object-fit: cover; flex-shrink: 0; } .gb-card-img-placeholder { background: var(--rs-bg-surface-raised); display: flex; align-items: center; justify-content: center; font-size: 1.5rem; } .gb-card-info { flex: 1; min-width: 0; } .gb-card-stats { display: flex; gap: 0.75rem; color: var(--rs-text-muted); font-size: 0.75rem; margin-top: 0.375rem; } .gb-card-progress { margin-bottom: 0.5rem; } .gb-card-progress-label { color: var(--rs-text-secondary); font-size: 0.75rem; margin-top: 0.25rem; } .gb-card-tiers { display: flex; gap: 0.375rem; flex-wrap: wrap; } .gb-tier-chip { padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.6875rem; background: var(--rs-bg-surface-raised); color: var(--rs-text-muted); border: 1px solid transparent; } .gb-tier-reached { color: #4ade80; background: rgba(34,197,94,0.08); } .gb-tier-current { border-color: var(--rs-primary); color: var(--rs-primary-hover); background: rgba(99,102,241,0.1); font-weight: 600; } @media (max-width: 768px) { .catalog-detail-layout { grid-template-columns: 1fr; } } @media (max-width: 600px) { .grid { grid-template-columns: 1fr; } .catalog-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } .url-input-row { flex-direction: column; } .detail-actions { flex-direction: column; } } `; } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } } customElements.define("folk-cart-shop", FolkCartShop);