diff --git a/modules/cart/components/folk-cart-shop.ts b/modules/cart/components/folk-cart-shop.ts index ae6bfdc..d1ff6ac 100644 --- a/modules/cart/components/folk-cart-shop.ts +++ b/modules/cart/components/folk-cart-shop.ts @@ -104,6 +104,30 @@ class FolkCartShop extends HTMLElement { 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 = [ diff --git a/modules/swag/components/folk-swag-designer.ts b/modules/swag/components/folk-swag-designer.ts index c47c703..03535b8 100644 --- a/modules/swag/components/folk-swag-designer.ts +++ b/modules/swag/components/folk-swag-designer.ts @@ -1,19 +1,167 @@ /** * — upload artwork → generate print-ready files. - * Product selector (sticker, poster, tee), image upload with preview, + * Product selector (sticker, poster, tee, hoodie), image upload with preview, * generate button, artifact result display with download link. + * + * Demo mode: 4-step interactive flow with inline SVG mockups, + * provider matching, revenue splits, and pipeline visualization. */ +// --- Demo data (self-contained, zero API calls in demo mode) --- + +interface DemoProduct { + id: string; + name: string; + printArea: string; + baseCost: string; + printful: boolean; + sizes?: string[]; + colors?: { id: string; name: string; hex: string }[]; +} + +interface DemoProvider { + name: string; + type: "cosmolocal" | "global"; + city: string; + lat: number; + lng: number; + capabilities: string[]; + unitCost: number; + turnaround: string; +} + +const DEMO_PRODUCTS: DemoProduct[] = [ + { + id: "tee", name: "T-Shirt", printArea: "305×406mm", baseCost: "$9.25–$13.25", + printful: true, + sizes: ["S", "M", "L", "XL", "2XL", "3XL"], + colors: [ + { id: "black", name: "Black", hex: "#0a0a0a" }, + { id: "white", name: "White", hex: "#ffffff" }, + { id: "forest_green", name: "Forest Green", hex: "#2d4a3e" }, + { id: "heather_charcoal", name: "Heather Charcoal", hex: "#4a4a4a" }, + { id: "maroon", name: "Maroon", hex: "#5a2d2d" }, + ], + }, + { + id: "sticker", name: "Sticker Sheet", printArea: "210×297mm", baseCost: "$1.20–$1.50", + printful: true, + }, + { + id: "poster", name: "Poster (A3)", printArea: "297×420mm", baseCost: "$4.50–$7.00", + printful: false, + }, + { + id: "hoodie", name: "Hoodie", printArea: "356×406mm", baseCost: "$23.95–$27.95", + printful: true, + sizes: ["S", "M", "L", "XL", "2XL"], + colors: [ + { id: "black", name: "Black", hex: "#0a0a0a" }, + { id: "dark_grey_heather", name: "Dark Grey Heather", hex: "#3a3a3a" }, + ], + }, +]; + +const DEMO_PROVIDERS: DemoProvider[] = [ + { name: "De Drukker Collective", type: "cosmolocal", city: "Amsterdam", lat: 52.37, lng: 4.90, capabilities: ["dtg-print", "vinyl-cut", "screen-print"], unitCost: 8.50, turnaround: "3-4 days" }, + { name: "Kuona Print Collective", type: "cosmolocal", city: "Nairobi", lat: -1.29, lng: 36.82, capabilities: ["dtg-print", "screen-print", "vinyl-cut"], unitCost: 5.50, turnaround: "5-7 days" }, + { name: "Grafica Popular", type: "cosmolocal", city: "Sao Paulo", lat: -23.55, lng: -46.63, capabilities: ["screen-print", "risograph", "inkjet-print"], unitCost: 6.00, turnaround: "4-6 days" }, + { name: "Printful", type: "global", city: "Global", lat: 0, lng: 0, capabilities: ["dtg-print", "vinyl-cut", "inkjet-print"], unitCost: 9.25, turnaround: "5-7 days" }, +]; + +const DEMO_BUYER = { lat: 52.52, lng: 13.41 }; // Berlin + +function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number { + const R = 6371; + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLng = (lng2 - lng1) * Math.PI / 180; + const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +} + +// --- SVG generators --- + +function cosmoDesignSvg(): string { + return ` + + + + + + ${Array.from({length: 12}, (_, i) => { + const a = i * 30 * Math.PI / 180; + return ``; + }).join("")} + + COSMOLOCAL + NETWORK + `; +} + +function teeMockupSvg(color: string): string { + return ` + + + + ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} + + + `; +} + +function hoodieMockupSvg(color: string): string { + return ` + + + + + + ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} + + + `; +} + +function stickerMockupSvg(): string { + return ` + + + + ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} + + kiss-cut border + `; +} + +function posterMockupSvg(): string { + return ` + + + + ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} + + COSMOLOCAL NETWORK + A3 — 297×420mm — 300 DPI + `; +} + +// --- Component --- + class FolkSwagDesigner extends HTMLElement { private shadow: ShadowRoot; private space = ""; - private selectedProduct = "sticker"; + private selectedProduct = "tee"; + private selectedSize = "M"; + private selectedColor = "black"; private imageFile: File | null = null; private imagePreview = ""; private designTitle = ""; private generating = false; private artifact: any = null; private error = ""; + private demoStep: 1 | 2 | 3 | 4 = 1; + private progressStep = 0; + private usedSampleDesign = false; constructor() { super(); @@ -23,73 +171,130 @@ class FolkSwagDesigner extends HTMLElement { connectedCallback() { this.space = this.getAttribute("space") || ""; if (this.space === "demo") { - this.loadDemoData(); + this.selectedProduct = "tee"; + this.selectedSize = "M"; + this.selectedColor = "black"; + this.designTitle = "Cosmolocal Network Tee"; + this.demoStep = 1; + this.render(); return; } this.render(); } - private loadDemoData() { - this.selectedProduct = "sticker"; - this.designTitle = "Cosmolocal Network"; - this.imagePreview = ""; - this.render(); - - requestAnimationFrame(() => { - // Set the title input value - const titleInput = this.shadow.querySelector(".title-input") as HTMLInputElement; - if (titleInput) titleInput.value = this.designTitle; - - // Show a demo artifact result - this.artifact = { - title: "Cosmolocal Network", - product: "sticker", - payload: { title: "Cosmolocal Network" }, - spec: { - product_type: "sticker", - dimensions: { width_mm: 76, height_mm: 76 }, - dpi: 300, - }, - render_targets: { - pdf: { format: "pdf", label: "Print-Ready PDF", size: "3x3 in", url: "#demo-pdf" }, - png: { format: "png", label: "Preview PNG", size: "900x900 px", url: "#demo-png" }, - }, - }; - this.render(); - }); - } - private getApiBase(): string { const path = window.location.pathname; const parts = path.split("/").filter(Boolean); return parts.length >= 2 ? `/${parts[0]}/swag` : "/demo/swag"; } + private getDemoProduct(): DemoProduct { + return DEMO_PRODUCTS.find(p => p.id === this.selectedProduct) || DEMO_PRODUCTS[0]; + } + + private getAutoTitle(): string { + const p = this.getDemoProduct(); + return `Cosmolocal Network ${p.name}`; + } + + private demoSelectProduct(id: string) { + this.selectedProduct = id; + const p = this.getDemoProduct(); + this.selectedSize = p.sizes?.[1] || ""; + this.selectedColor = p.colors?.[0]?.id || ""; + this.designTitle = this.getAutoTitle(); + this.demoStep = 1; + this.usedSampleDesign = false; + this.artifact = null; + this.render(); + } + + private demoUseSample() { + this.usedSampleDesign = true; + this.demoStep = 2; + this.render(); + } + + private demoGenerate() { + this.demoStep = 3; + this.progressStep = 0; + this.render(); + + const steps = [1, 2, 3, 4]; + const delays = [400, 400, 400, 300]; + let elapsed = 0; + for (const s of steps) { + elapsed += delays[s - 1]; + setTimeout(() => { + this.progressStep = s; + if (s === 4) { + this.buildDemoArtifact(); + this.demoStep = 4; + } + this.render(); + }, elapsed); + } + } + + private buildDemoArtifact() { + const p = this.getDemoProduct(); + const dims: Record = { + tee: { w: 305, h: 406 }, sticker: { w: 210, h: 297 }, + poster: { w: 297, h: 420 }, hoodie: { w: 356, h: 406 }, + }; + const d = dims[p.id] || dims.tee; + const wpx = Math.round((d.w / 25.4) * 300); + const hpx = Math.round((d.h / 25.4) * 300); + + const capabilities: Record = { + tee: ["dtg-print"], sticker: ["vinyl-cut"], poster: ["inkjet-print"], hoodie: ["dtg-print"], + }; + const substrates: Record = { + tee: ["cotton-standard", "cotton-organic"], sticker: ["vinyl-matte", "vinyl-gloss"], + poster: ["paper-160gsm-cover", "paper-100gsm"], hoodie: ["cotton-polyester-blend"], + }; + + this.artifact = { + title: this.designTitle || this.getAutoTitle(), + product: p.id, + spec: { + product_type: p.id === "sticker" ? "sticker-sheet" : p.id, + dimensions: { width_mm: d.w, height_mm: d.h }, + dpi: 300, + color_space: "sRGB", + required_capabilities: capabilities[p.id], + substrates: substrates[p.id], + }, + render_targets: { + print: { format: "png", label: "Print-Ready PNG", dimensions: `${wpx}x${hpx} px`, url: "#demo-print" }, + preview: { format: "png", label: "Preview PNG", dimensions: `${Math.round(wpx / 4)}x${Math.round(hpx / 4)} px`, url: "#demo-preview" }, + }, + pricing: { creator_share: "35%", community_share: "15%", provider_share: "50%" }, + next_actions: [ + { action: "ingest_to_rcart", label: "Send to rCart", url: "/demo/cart" }, + { action: "edit_design", label: "Edit Design" }, + { action: "save_to_rfiles", label: "Save to rFiles" }, + ], + ...(this.selectedSize ? { size: this.selectedSize } : {}), + ...(this.selectedColor ? { color: this.selectedColor } : {}), + }; + } + + private getMatchedProviders(): (DemoProvider & { distance: number })[] { + const p = this.getDemoProduct(); + const cap = p.id === "sticker" ? "vinyl-cut" : p.id === "poster" ? "inkjet-print" : "dtg-print"; + return DEMO_PROVIDERS + .filter(prov => prov.capabilities.includes(cap)) + .map(prov => ({ + ...prov, + distance: prov.type === "global" ? Infinity : Math.round(haversineKm(DEMO_BUYER.lat, DEMO_BUYER.lng, prov.lat, prov.lng)), + })) + .sort((a, b) => a.distance - b.distance); + } + private async generate() { if (this.space === "demo") { - this.generating = true; - this.error = ""; - this.artifact = null; - this.render(); - // Simulate a short delay, then show demo artifact - setTimeout(() => { - this.artifact = { - title: this.designTitle || "Untitled Design", - product: this.selectedProduct, - payload: { title: this.designTitle || "Untitled Design" }, - spec: { - product_type: this.selectedProduct, - dimensions: { width_mm: this.selectedProduct === "tee" ? 305 : this.selectedProduct === "poster" ? 297 : 76, height_mm: this.selectedProduct === "tee" ? 406 : this.selectedProduct === "poster" ? 420 : 76 }, - dpi: 300, - }, - render_targets: { - pdf: { format: "pdf", label: "Print-Ready PDF", size: this.selectedProduct === "tee" ? "12x16 in" : this.selectedProduct === "poster" ? "A3" : "3x3 in", url: "#demo-pdf" }, - png: { format: "png", label: "Preview PNG", size: this.selectedProduct === "tee" ? "3600x4800 px" : this.selectedProduct === "poster" ? "3508x4961 px" : "900x900 px", url: "#demo-png" }, - }, - }; - this.generating = false; - this.render(); - }, 800); + this.demoGenerate(); return; } if (!this.imageFile || this.generating) return; @@ -124,16 +329,255 @@ class FolkSwagDesigner extends HTMLElement { } private render() { + if (this.space === "demo") { + this.renderDemo(); + } else { + this.renderFull(); + } + } + + // ---- Demo mode rendering (4-step flow) ---- + + private renderDemo() { + const p = this.getDemoProduct(); + const isApparel = p.id === "tee" || p.id === "hoodie"; + + this.shadow.innerHTML = ` + + + +
+ ${[ + { n: 1, label: "Product" }, + { n: 2, label: "Design" }, + { n: 3, label: "Generate" }, + { n: 4, label: "Pipeline" }, + ].map(s => `
+ ${this.demoStep > s.n ? '✓' : s.n} + ${s.label} +
`).join('
')} +
+ + +
+
+ ${DEMO_PRODUCTS.map(dp => ` +
+
${this.productIcon(dp.id)}
+
${dp.name}
+
${dp.printArea}
+
${dp.baseCost}
+ ${dp.printful ? 'Printful' : 'cosmolocal only'} +
+ `).join("")} +
+ + ${isApparel && p.sizes ? ` +
+ Size +
+ ${p.sizes.map(s => ``).join("")} +
+
` : ""} + + ${isApparel && p.colors ? ` +
+ Color +
+ ${p.colors.map(c => ``).join("")} +
+
` : ""} +
+ + +
+
+
${this.getMockupSvg()}
+
+

${this.esc(this.designTitle || this.getAutoTitle())}

+
${p.name} ${this.selectedSize ? `/ ${this.selectedSize}` : ""} ${this.selectedColor && p.colors ? `/ ${p.colors.find(c => c.id === this.selectedColor)?.name || ""}` : ""}
+ ${!this.usedSampleDesign ? `` : 'Sample design loaded'} +
+
+
+ + + ${this.demoStep === 2 && this.usedSampleDesign ? ` +
+ +
` : ""} + + + ${this.demoStep >= 3 ? this.renderStep3() : ""} + + + ${this.demoStep >= 4 ? this.renderStep4() : ""} + `; + + this.bindDemoEvents(); + } + + private renderStep3(): string { + const progressLabels = ["Processing image...", "Generating artifact...", "Matching providers...", "Done!"]; + + let html = `
`; + + // Progress bar (always show) + html += `
+
+
+
+ ${progressLabels.map((label, i) => `${this.progressStep > i + 1 ? '✓ ' : ''}${label}`).join("")} +
`; + + // Only show artifact + providers once done + if (this.demoStep >= 4 && this.artifact) { + const providers = this.getMatchedProviders(); + const selectedProvider = providers[0]; + const unitCost = selectedProvider?.unitCost || 9.25; + const provAmt = (unitCost * 0.5).toFixed(2); + const creatorAmt = (unitCost * 0.35).toFixed(2); + const communityAmt = (unitCost * 0.15).toFixed(2); + + // Artifact card + html += ` +
+

Artifact Envelope

+
+
Product${this.esc(this.artifact.spec.product_type)}
+
Dimensions${this.artifact.spec.dimensions.width_mm}×${this.artifact.spec.dimensions.height_mm}mm
+
DPI${this.artifact.spec.dpi}
+
Color Space${this.artifact.spec.color_space}
+
Capabilities${(this.artifact.spec.required_capabilities || []).join(", ")}
+
Substrates${(this.artifact.spec.substrates || []).join(", ")}
+
+
+ ${Object.values(this.artifact.render_targets).map((t: any) => `${t.label}: ${t.dimensions}`).join("")} +
+
+ ${(this.artifact.next_actions || []).map((a: any) => `${a.label}`).join("")} +
+
`; + + // Provider match table + html += ` +
+

Provider Matching (buyer: Berlin)

+
+
+ ProviderTypeCityDistanceCostTurnaround +
+ ${providers.map((prov, i) => ` +
+ ${prov.name} + ${prov.type} + ${prov.city} + ${prov.distance === Infinity ? '--' : `~${prov.distance.toLocaleString()} km`} + $${prov.unitCost.toFixed(2)} + ${prov.turnaround} +
`).join("")} +
+
`; + + // Revenue split bar + html += ` +
+

Revenue Split (from $${unitCost.toFixed(2)} unit cost)

+
+
Provider 50% $${provAmt}
+
Creator 35% $${creatorAmt}
+
$${communityAmt}
+
+
Community 15%
+
`; + } + + html += `
`; + return html; + } + + private renderStep4(): string { + return ` +
+

Print-on-Demand Pipeline

+
+
${this.productIcon("tee")}
rSwag Design
+
+
Artifact Spec
+
+
rCart Catalog
+
+
Provider Match
+
+
Local Print
+
+
+ Send to rCart + +
+
${this.esc(JSON.stringify(this.artifact, null, 2))}
+
`; + } + + private productIcon(id: string): string { + const icons: Record = { + tee: ``, + sticker: ``, + poster: ``, + hoodie: ``, + }; + return icons[id] || icons.tee; + } + + private getMockupSvg(): string { + const colorHex = this.getDemoProduct().colors?.find(c => c.id === this.selectedColor)?.hex || "#0a0a0a"; + switch (this.selectedProduct) { + case "tee": return teeMockupSvg(colorHex); + case "hoodie": return hoodieMockupSvg(colorHex); + case "sticker": return stickerMockupSvg(); + case "poster": return posterMockupSvg(); + default: return teeMockupSvg(colorHex); + } + } + + private bindDemoEvents() { + // Product selection + this.shadow.querySelectorAll(".product").forEach(el => { + el.addEventListener("click", () => this.demoSelectProduct((el as HTMLElement).dataset.product || "tee")); + }); + // Size pills + this.shadow.querySelectorAll(".pill[data-size]").forEach(el => { + el.addEventListener("click", () => { this.selectedSize = (el as HTMLElement).dataset.size || "M"; this.render(); }); + }); + // Color swatches + this.shadow.querySelectorAll(".swatch[data-color]").forEach(el => { + el.addEventListener("click", () => { this.selectedColor = (el as HTMLElement).dataset.color || "black"; this.render(); }); + }); + // Sample design button + this.shadow.querySelector(".sample-btn")?.addEventListener("click", () => this.demoUseSample()); + // Generate button + this.shadow.querySelector(".generate-btn")?.addEventListener("click", () => this.demoGenerate()); + // JSON toggle + this.shadow.querySelector(".json-toggle-btn")?.addEventListener("click", () => { + const pre = this.shadow.querySelector(".json-pre"); + pre?.classList.toggle("visible"); + }); + } + + // ---- Full (non-demo) rendering ---- + + private renderFull() { const products = [ { id: "sticker", name: "Sticker Sheet", icon: "📋", desc: "A4 vinyl stickers" }, { id: "poster", name: "Poster (A3)", icon: "🖼", desc: "A3 art print" }, - { id: "tee", name: "T-Shirt", icon: "👕", desc: "12x16\" DTG print" }, + { id: "tee", name: "T-Shirt", icon: "👕", desc: '12x16" DTG print' }, + { id: "hoodie", name: "Hoodie", icon: "🧥", desc: '14x16" DTG print' }, ]; this.shadow.innerHTML = ` @@ -182,33 +626,33 @@ class FolkSwagDesigner extends HTMLElement {
${this.imagePreview ? `Preview` - : `
📁 Click or drag to upload artwork (PNG, JPG, SVG)
`} + : `
Click or drag to upload artwork (PNG, JPG, SVG)
`}
- ${this.error ? `
${this.esc(this.error)}
` : ""} ${this.artifact ? `
-

✅ ${this.esc(this.artifact.payload?.title || "Artifact")}

+

${this.esc(this.artifact.payload?.title || this.artifact.title || "Artifact")}

- ${this.esc(this.artifact.spec?.product_type || "")} • - ${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm • + ${this.esc(this.artifact.spec?.product_type || "")} • + ${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm • ${this.artifact.spec?.dpi}dpi
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => ` - ⬇ Download ${target.format.toUpperCase()} + Download ${target.format.toUpperCase()} `).join("")} - +
- Show artifact envelope ▼ + Show artifact envelope
${this.esc(JSON.stringify(this.artifact, null, 2))}
` : ""} `; @@ -249,6 +693,145 @@ class FolkSwagDesigner extends HTMLElement { }); } + private getDemoStyles(): string { + return ` + :host { display: block; padding: 1.5rem; max-width: 960px; margin: 0 auto; } + *, *::before, *::after { box-sizing: border-box; } + + /* Step bar */ + .steps-bar { display: flex; align-items: center; justify-content: center; gap: 0; margin-bottom: 2rem; } + .step-dot { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; } + .step-num { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; border: 2px solid #334155; color: #64748b; background: #1e293b; transition: all 0.2s; } + .step-dot.active .step-num { border-color: #6366f1; color: #fff; background: #4f46e5; } + .step-dot.current .step-num { box-shadow: 0 0 0 3px rgba(99,102,241,0.3); } + .step-label { font-size: 0.6875rem; color: #64748b; } + .step-dot.active .step-label { color: #a5b4fc; } + .step-line { width: 40px; height: 2px; background: #334155; margin: 0 0.5rem; margin-bottom: 1rem; } + + /* Product grid */ + .products { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; } + .product { padding: 0.875rem 0.5rem; border-radius: 12px; border: 2px solid #334155; background: #1e293b; cursor: pointer; text-align: center; transition: all 0.15s; } + .product:hover { border-color: #475569; } + .product.active { border-color: #6366f1; background: rgba(99,102,241,0.1); } + .product-icon { margin-bottom: 0.375rem; display: flex; justify-content: center; } + .product-name { color: #f1f5f9; font-weight: 600; font-size: 0.8125rem; } + .product-specs { color: #64748b; font-size: 0.6875rem; margin-top: 0.125rem; } + .product-cost { color: #94a3b8; font-size: 0.75rem; margin-top: 0.25rem; } + + /* Badges */ + .badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.625rem; font-weight: 600; margin-top: 0.375rem; } + .badge-printful { background: rgba(56,189,248,0.15); color: #38bdf8; } + .badge-local { background: rgba(34,197,94,0.15); color: #4ade80; } + .badge-cosmo { background: rgba(34,197,94,0.15); color: #4ade80; } + .badge-global { background: rgba(56,189,248,0.15); color: #38bdf8; } + .badge-ok { background: rgba(34,197,94,0.15); color: #4ade80; padding: 0.25rem 0.75rem; font-size: 0.75rem; } + + /* Options */ + .option-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; } + .option-label { color: #94a3b8; font-size: 0.8125rem; font-weight: 500; min-width: 40px; } + .pills { display: flex; gap: 0.375rem; flex-wrap: wrap; } + .pill { padding: 0.375rem 0.75rem; border-radius: 999px; border: 1px solid #334155; background: #1e293b; color: #94a3b8; font-size: 0.75rem; cursor: pointer; transition: all 0.15s; } + .pill:hover { border-color: #475569; color: #f1f5f9; } + .pill.active { border-color: #6366f1; background: rgba(99,102,241,0.15); color: #fff; } + .color-swatches { display: flex; gap: 0.5rem; } + .swatch { width: 28px; height: 28px; border-radius: 50%; border: 2px solid #334155; cursor: pointer; transition: all 0.15s; } + .swatch:hover { border-color: #475569; } + .swatch.active { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99,102,241,0.3); } + + /* Mockup */ + .mockup-area { display: flex; gap: 1.5rem; align-items: center; background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; } + .mockup-svg { flex-shrink: 0; } + .mockup-svg svg { width: 180px; height: auto; } + .mockup-info { flex: 1; } + .mockup-title { color: #f1f5f9; font-weight: 600; font-size: 1rem; margin: 0 0 0.25rem; } + .mockup-meta { color: #94a3b8; font-size: 0.8125rem; margin-bottom: 0.75rem; } + + /* Buttons */ + .btn { padding: 0.625rem 1.25rem; border-radius: 8px; font-size: 0.875rem; font-weight: 600; cursor: pointer; border: none; text-decoration: none; display: inline-block; transition: all 0.15s; } + .btn-primary { background: #4f46e5; color: #fff; } + .btn-primary:hover { background: #4338ca; } + .btn-secondary { background: #334155; color: #f1f5f9; } + .btn-secondary:hover { background: #475569; } + .generate-row { text-align: center; margin-bottom: 1.5rem; } + + /* Step sections */ + .step-section { margin-bottom: 1.5rem; } + .step-section:not(.visible) { display: none; } + + /* Progress bar */ + .progress-bar { height: 6px; background: #1e293b; border-radius: 3px; overflow: hidden; margin-bottom: 0.5rem; } + .progress-fill { height: 100%; background: linear-gradient(90deg, #6366f1, #818cf8); border-radius: 3px; transition: width 0.3s ease; } + .progress-steps { display: flex; justify-content: space-between; margin-bottom: 1.5rem; } + .prog-label { font-size: 0.6875rem; color: #475569; transition: color 0.2s; } + .prog-label.active { color: #818cf8; font-weight: 600; } + .prog-label.done { color: #4ade80; } + + /* Artifact card */ + .artifact-card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; } + .artifact-heading { color: #f1f5f9; font-weight: 600; font-size: 0.9375rem; margin: 0 0 0.75rem; } + .artifact-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem 1rem; margin-bottom: 0.75rem; } + .artifact-field { display: flex; flex-direction: column; } + .af-label { color: #64748b; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; } + .af-value { color: #e2e8f0; font-size: 0.8125rem; } + .artifact-targets { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem; } + .target-chip { background: rgba(99,102,241,0.1); color: #a5b4fc; padding: 0.25rem 0.625rem; border-radius: 6px; font-size: 0.6875rem; } + .artifact-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; } + .action-chip { background: #0f172a; color: #94a3b8; padding: 0.25rem 0.625rem; border-radius: 6px; font-size: 0.6875rem; border: 1px solid #334155; } + + /* Provider table */ + .provider-section { margin-bottom: 1rem; } + .provider-heading { color: #f1f5f9; font-weight: 600; font-size: 0.9375rem; margin: 0 0 0.75rem; } + .buyer-loc { color: #64748b; font-weight: 400; font-size: 0.75rem; } + .provider-table { background: #1e293b; border: 1px solid #334155; border-radius: 12px; overflow: hidden; } + .pt-header, .pt-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1.2fr 0.8fr 1fr; padding: 0.625rem 1rem; font-size: 0.8125rem; align-items: center; } + .pt-header { background: #0f172a; color: #64748b; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; } + .pt-row { color: #e2e8f0; border-top: 1px solid #1e293b; } + .pt-row.nearest { background: rgba(99,102,241,0.05); border-left: 3px solid #6366f1; } + .pt-name { font-weight: 500; } + .pt-cost { color: #4ade80; font-weight: 600; } + + /* Split bar */ + .split-section { margin-bottom: 1.5rem; } + .split-heading { color: #94a3b8; font-size: 0.8125rem; font-weight: 500; margin: 0 0 0.5rem; } + .split-total { color: #64748b; font-weight: 400; } + .split-bar { display: flex; height: 32px; border-radius: 8px; overflow: hidden; font-size: 0.6875rem; font-weight: 600; } + .split-seg { display: flex; align-items: center; justify-content: center; gap: 0.25rem; color: #fff; } + .split-provider { background: #16a34a; } + .split-creator { background: #4f46e5; } + .split-community { background: #d97706; } + .split-amt { opacity: 0.85; } + .split-legend { text-align: right; margin-top: 0.25rem; } + .split-legend span { color: #64748b; font-size: 0.6875rem; } + + /* Pipeline */ + .pipeline-heading { color: #f1f5f9; font-weight: 600; font-size: 0.9375rem; margin: 0 0 1rem; text-align: center; } + .pipeline { display: flex; align-items: center; justify-content: center; gap: 0; flex-wrap: wrap; margin-bottom: 1.5rem; } + .pipe-node { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; padding: 0.75rem; background: #1e293b; border: 1px solid #334155; border-radius: 10px; position: relative; min-width: 90px; } + .pipe-node.done { border-color: #22c55e; } + .pipe-icon { display: flex; align-items: center; justify-content: center; } + .pipe-label { color: #e2e8f0; font-size: 0.6875rem; font-weight: 500; text-align: center; } + .pipe-check { position: absolute; top: -6px; right: -6px; background: #16a34a; color: #fff; width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.5625rem; } + .pipe-arrow { color: #475569; font-size: 1.25rem; margin: 0 0.25rem; margin-bottom: 0.75rem; } + + /* Pipeline actions */ + .pipeline-actions { display: flex; gap: 0.75rem; justify-content: center; margin-bottom: 1rem; } + + /* JSON */ + .json-pre { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 0.75rem; overflow-x: auto; font-size: 0.6875rem; color: #94a3b8; max-height: 300px; display: none; white-space: pre-wrap; word-break: break-all; } + .json-pre.visible { display: block; } + + @media (max-width: 768px) { + .products { grid-template-columns: repeat(2, 1fr); } + .mockup-area { flex-direction: column; text-align: center; } + .pt-header, .pt-row { grid-template-columns: 1.5fr 0.8fr 0.8fr 1fr 0.7fr 0.8fr; font-size: 0.6875rem; padding: 0.5rem; } + .artifact-grid { grid-template-columns: repeat(2, 1fr); } + .pipeline { gap: 0.25rem; } + .pipe-node { min-width: 70px; padding: 0.5rem; } + .pipe-label { font-size: 0.5625rem; } + } + `; + } + private esc(s: string): string { const d = document.createElement("div"); d.textContent = s; diff --git a/modules/swag/landing.ts b/modules/swag/landing.ts index 36d1ba2..c284aae 100644 --- a/modules/swag/landing.ts +++ b/modules/swag/landing.ts @@ -72,6 +72,81 @@ export function renderLanding(): string { + +
+
+

Design Global, Manufacture Local

+

One artifact spec powers the entire pipeline — from design to local fulfillment.

+
+
+
1
+

Design in rSwag

+

Pick a product, upload artwork, and configure sizes and colors.

+
+
+
2
+

Generate Artifact

+

Print-ready files with specs, DPI, bleed, color profiles, and metadata.

+
+
+
3
+

List on rCart

+

The artifact becomes a catalog product, ready for community ordering.

+
+
+
4
+

Match Provider

+

Nearest capable local print shop gets the job. Printful as global fallback.

+
+
+
5
+

Print & Ship Locally

+

Community printer fulfills the order. Revenue auto-splits to all parties.

+
+
+
+

The same artifact envelope that powers pocket book publishing also drives swag fulfillment. One spec, many products.

+
+
+
+ + +
+
+

Local + Global Fulfillment

+

Community printers first, global POD as fallback. Always fulfilled.

+
+
+
+ cosmolocal +

Community Print Network

+
+

6 community print shops across 4 continents. Matched by proximity and capability. Lower cost, faster delivery, supports the local economy.

+

DTG, screen print, vinyl cut, risograph, inkjet — capabilities vary by shop.

+
+
+
+ global +

Printful (Global Fallback)

+
+

DTG apparel, vinyl stickers, and art prints shipped worldwide. Always available. Bella+Canvas blanks. Sandbox mode for testing.

+

SKU 71 (tee), SKU 146 (hoodie), SKU 358 (sticker) — full size and color ranges.

+
+
+
+

Revenue Split on Every Order

+
+
Provider 50%
+
Creator 35%
+
15%
+
+
+ Community 15% +
+
+
+
+
diff --git a/modules/swag/mod.ts b/modules/swag/mod.ts index d353c96..197ab7a 100644 --- a/modules/swag/mod.ts +++ b/modules/swag/mod.ts @@ -35,6 +35,7 @@ routes.get("/api/products", (c) => { substrates: p.substrates, requiredCapabilities: p.requiredCapabilities, finish: p.finish, + ...(p.printful ? { printful: p.printful } : {}), })); return c.json({ products }); }); diff --git a/modules/swag/products.ts b/modules/swag/products.ts index aa3a42f..cef06f3 100644 --- a/modules/swag/products.ts +++ b/modules/swag/products.ts @@ -1,3 +1,9 @@ +export interface PrintfulMeta { + sku: number; + sizes?: string[]; + colors?: { id: string; name: string; hex: string }[]; +} + export interface ProductTemplate { id: string; name: string; @@ -13,6 +19,8 @@ export interface ProductTemplate { substrates: string[]; requiredCapabilities: string[]; finish: string; + // Printful product metadata (optional — cosmolocal-only products omit this) + printful?: PrintfulMeta; // Computed pixel dimensions (at DPI) get widthPx(): number; get heightPx(): number; @@ -42,6 +50,7 @@ export const PRODUCTS: Record = { substrates: ["vinyl-matte", "vinyl-gloss", "sticker-paper-matte"], requiredCapabilities: ["vinyl-cut"], finish: "matte", + printful: { sku: 358 }, }), poster: makeTemplate({ id: "poster", @@ -66,6 +75,37 @@ export const PRODUCTS: Record = { substrates: ["cotton-standard", "cotton-organic"], requiredCapabilities: ["dtg-print"], finish: "none", + printful: { + sku: 71, + sizes: ["S", "M", "L", "XL", "2XL", "3XL"], + colors: [ + { id: "black", name: "Black", hex: "#0a0a0a" }, + { id: "white", name: "White", hex: "#ffffff" }, + { id: "forest_green", name: "Forest Green", hex: "#2d4a3e" }, + { id: "heather_charcoal", name: "Heather Charcoal", hex: "#4a4a4a" }, + { id: "maroon", name: "Maroon", hex: "#5a2d2d" }, + ], + }, + }), + hoodie: makeTemplate({ + id: "hoodie", + name: "Hoodie", + description: "Front print on pullover hoodie (14x16 inch print area)", + printArea: { widthMm: 356, heightMm: 406 }, + dpi: 300, + bleedMm: 0, + productType: "hoodie", + substrates: ["cotton-polyester-blend", "cotton-organic"], + requiredCapabilities: ["dtg-print"], + finish: "none", + printful: { + sku: 146, + sizes: ["S", "M", "L", "XL", "2XL"], + colors: [ + { id: "black", name: "Black", hex: "#0a0a0a" }, + { id: "dark_grey_heather", name: "Dark Grey Heather", hex: "#3a3a3a" }, + ], + }, }), };