From 7e5a8624d7b24746fd51a492d78d887bea4824c5 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sat, 28 Feb 2026 06:48:10 +0000 Subject: [PATCH] feat: rSwag design-on-demand demo with Printful hybrid pipeline Add hoodie product + Printful metadata (SKU/sizes/colors) to product catalog. Rewrite swag designer demo with 4-step interactive flow: product selection with size/color pickers, inline SVG mockups, artifact generation with provider matching (cosmolocal + Printful fallback sorted by distance), revenue split visualization, and pipeline diagram. Add cosmolocal tee and sticker sheet to cart demo catalog. Add pipeline and fulfillment sections to swag landing page. Co-Authored-By: Claude Opus 4.6 --- modules/cart/components/folk-cart-shop.ts | 24 + modules/swag/components/folk-swag-designer.ts | 719 ++++++++++++++++-- modules/swag/landing.ts | 75 ++ modules/swag/mod.ts | 1 + modules/swag/products.ts | 40 + 5 files changed, 791 insertions(+), 68 deletions(-) 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" }, + ], + }, }), };