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 <noreply@anthropic.com>
This commit is contained in:
parent
f1e90924c0
commit
7e5a8624d7
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -1,19 +1,167 @@
|
|||
/**
|
||||
* <folk-swag-designer> — 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 `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="200" height="200" fill="#0f172a"/>
|
||||
<circle cx="100" cy="100" r="70" fill="none" stroke="#6366f1" stroke-width="1.5" opacity="0.4"/>
|
||||
<circle cx="100" cy="100" r="50" fill="none" stroke="#818cf8" stroke-width="1.5" opacity="0.6"/>
|
||||
<circle cx="100" cy="100" r="30" fill="none" stroke="#a5b4fc" stroke-width="1.5" opacity="0.8"/>
|
||||
<circle cx="100" cy="100" r="10" fill="#6366f1"/>
|
||||
${Array.from({length: 12}, (_, i) => {
|
||||
const a = i * 30 * Math.PI / 180;
|
||||
return `<line x1="${100 + 15 * Math.cos(a)}" y1="${100 + 15 * Math.sin(a)}" x2="${100 + 75 * Math.cos(a)}" y2="${100 + 75 * Math.sin(a)}" stroke="#818cf8" stroke-width="1" opacity="0.3"/>`;
|
||||
}).join("")}
|
||||
<circle cx="100" cy="100" r="5" fill="#e2e8f0"/>
|
||||
<text x="100" y="160" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">COSMOLOCAL</text>
|
||||
<text x="100" y="170" text-anchor="middle" fill="#64748b" font-size="6" font-family="system-ui">NETWORK</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function teeMockupSvg(color: string): string {
|
||||
return `<svg viewBox="0 0 240 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M60,40 L30,60 L10,120 L40,130 L50,90 L50,280 L190,280 L190,90 L200,130 L230,120 L210,60 L180,40 L160,55 Q140,70 120,70 Q100,70 80,55 Z" fill="${color}" stroke="#475569" stroke-width="1.5"/>
|
||||
<defs><clipPath id="tee-clip"><rect x="80" y="90" width="80" height="107" rx="4"/></clipPath></defs>
|
||||
<g clip-path="url(#tee-clip)" transform="translate(80,90)">
|
||||
<svg viewBox="0 0 200 200" width="80" height="80" y="13">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||||
</g>
|
||||
<rect x="80" y="90" width="80" height="107" rx="4" fill="none" stroke="#6366f1" stroke-width="0.75" stroke-dasharray="3,2" opacity="0.5"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function hoodieMockupSvg(color: string): string {
|
||||
return `<svg viewBox="0 0 260 310" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M65,50 L30,70 L5,140 L40,150 L55,100 L55,290 L205,290 L205,100 L220,150 L255,140 L230,70 L195,50 L175,60 Q155,80 130,85 Q105,80 85,60 Z" fill="${color}" stroke="#475569" stroke-width="1.5"/>
|
||||
<path d="M85,60 Q105,30 130,25 Q155,30 175,60 Q155,80 130,85 Q105,80 85,60 Z" fill="${color}" stroke="#475569" stroke-width="1"/>
|
||||
<ellipse cx="130" cy="55" rx="20" ry="15" fill="#1e293b" stroke="#475569" stroke-width="1"/>
|
||||
<defs><clipPath id="hoodie-clip"><rect x="80" y="100" width="100" height="115" rx="4"/></clipPath></defs>
|
||||
<g clip-path="url(#hoodie-clip)" transform="translate(80,100)">
|
||||
<svg viewBox="0 0 200 200" width="100" height="100" y="8">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||||
</g>
|
||||
<rect x="80" y="100" width="100" height="115" rx="4" fill="none" stroke="#6366f1" stroke-width="0.75" stroke-dasharray="3,2" opacity="0.5"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function stickerMockupSvg(): string {
|
||||
return `<svg viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="20" y="20" width="200" height="200" rx="16" fill="#1e293b" stroke="#475569" stroke-width="1.5"/>
|
||||
<rect x="30" y="30" width="180" height="180" rx="12" fill="none" stroke="#6366f1" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>
|
||||
<g transform="translate(40,40)">
|
||||
<svg viewBox="0 0 200 200" width="160" height="160">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||||
</g>
|
||||
<text x="120" y="232" text-anchor="middle" fill="#64748b" font-size="7" font-family="system-ui">kiss-cut border</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function posterMockupSvg(): string {
|
||||
return `<svg viewBox="0 0 220 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="15" y="15" width="190" height="270" fill="#1e293b" stroke="#475569" stroke-width="2"/>
|
||||
<rect x="25" y="25" width="170" height="250" fill="none" stroke="#334155" stroke-width="1"/>
|
||||
<g transform="translate(40,50)">
|
||||
<svg viewBox="0 0 200 200" width="140" height="140">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||||
</g>
|
||||
<text x="110" y="230" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui" font-weight="600">COSMOLOCAL NETWORK</text>
|
||||
<text x="110" y="245" text-anchor="middle" fill="#64748b" font-size="7" font-family="system-ui">A3 — 297×420mm — 300 DPI</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
// --- 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<string, { w: number; h: number }> = {
|
||||
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<string, string[]> = {
|
||||
tee: ["dtg-print"], sticker: ["vinyl-cut"], poster: ["inkjet-print"], hoodie: ["dtg-print"],
|
||||
};
|
||||
const substrates: Record<string, string[]> = {
|
||||
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 = `
|
||||
<style>${this.getDemoStyles()}</style>
|
||||
|
||||
<!-- Step indicators -->
|
||||
<div class="steps-bar">
|
||||
${[
|
||||
{ n: 1, label: "Product" },
|
||||
{ n: 2, label: "Design" },
|
||||
{ n: 3, label: "Generate" },
|
||||
{ n: 4, label: "Pipeline" },
|
||||
].map(s => `<div class="step-dot ${this.demoStep >= s.n ? 'active' : ''} ${this.demoStep === s.n ? 'current' : ''}">
|
||||
<span class="step-num">${this.demoStep > s.n ? '✓' : s.n}</span>
|
||||
<span class="step-label">${s.label}</span>
|
||||
</div>`).join('<div class="step-line"></div>')}
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Product Selection -->
|
||||
<section class="step-section ${this.demoStep >= 1 ? 'visible' : ''}">
|
||||
<div class="products">
|
||||
${DEMO_PRODUCTS.map(dp => `
|
||||
<div class="product ${this.selectedProduct === dp.id ? 'active' : ''}" data-product="${dp.id}">
|
||||
<div class="product-icon">${this.productIcon(dp.id)}</div>
|
||||
<div class="product-name">${dp.name}</div>
|
||||
<div class="product-specs">${dp.printArea}</div>
|
||||
<div class="product-cost">${dp.baseCost}</div>
|
||||
${dp.printful ? '<span class="badge badge-printful">Printful</span>' : '<span class="badge badge-local">cosmolocal only</span>'}
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
|
||||
${isApparel && p.sizes ? `
|
||||
<div class="option-row">
|
||||
<span class="option-label">Size</span>
|
||||
<div class="pills">
|
||||
${p.sizes.map(s => `<button class="pill ${this.selectedSize === s ? 'active' : ''}" data-size="${s}">${s}</button>`).join("")}
|
||||
</div>
|
||||
</div>` : ""}
|
||||
|
||||
${isApparel && p.colors ? `
|
||||
<div class="option-row">
|
||||
<span class="option-label">Color</span>
|
||||
<div class="color-swatches">
|
||||
${p.colors.map(c => `<button class="swatch ${this.selectedColor === c.id ? 'active' : ''}" data-color="${c.id}" style="background:${c.hex}" title="${c.name}"></button>`).join("")}
|
||||
</div>
|
||||
</div>` : ""}
|
||||
</section>
|
||||
|
||||
<!-- Step 2: Design Mockup -->
|
||||
<section class="step-section ${this.demoStep >= 2 ? 'visible' : ''}">
|
||||
<div class="mockup-area">
|
||||
<div class="mockup-svg">${this.getMockupSvg()}</div>
|
||||
<div class="mockup-info">
|
||||
<h3 class="mockup-title">${this.esc(this.designTitle || this.getAutoTitle())}</h3>
|
||||
<div class="mockup-meta">${p.name} ${this.selectedSize ? `/ ${this.selectedSize}` : ""} ${this.selectedColor && p.colors ? `/ ${p.colors.find(c => c.id === this.selectedColor)?.name || ""}` : ""}</div>
|
||||
${!this.usedSampleDesign ? `<button class="btn btn-secondary sample-btn">Use Sample Design</button>` : '<span class="badge badge-ok">Sample design loaded</span>'}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Generate button (between step 2 and 3) -->
|
||||
${this.demoStep === 2 && this.usedSampleDesign ? `
|
||||
<div class="generate-row">
|
||||
<button class="btn btn-primary generate-btn">Generate Print-Ready Files</button>
|
||||
</div>` : ""}
|
||||
|
||||
<!-- Step 3: Progress + Artifact + Providers -->
|
||||
${this.demoStep >= 3 ? this.renderStep3() : ""}
|
||||
|
||||
<!-- Step 4: Pipeline Visualization -->
|
||||
${this.demoStep >= 4 ? this.renderStep4() : ""}
|
||||
`;
|
||||
|
||||
this.bindDemoEvents();
|
||||
}
|
||||
|
||||
private renderStep3(): string {
|
||||
const progressLabels = ["Processing image...", "Generating artifact...", "Matching providers...", "Done!"];
|
||||
|
||||
let html = `<section class="step-section visible">`;
|
||||
|
||||
// Progress bar (always show)
|
||||
html += `<div class="progress-bar">
|
||||
<div class="progress-fill" style="width:${this.progressStep * 25}%"></div>
|
||||
</div>
|
||||
<div class="progress-steps">
|
||||
${progressLabels.map((label, i) => `<span class="prog-label ${this.progressStep > i ? 'done' : this.progressStep === i + 1 ? 'active' : ''}">${this.progressStep > i + 1 ? '✓ ' : ''}${label}</span>`).join("")}
|
||||
</div>`;
|
||||
|
||||
// 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 += `
|
||||
<div class="artifact-card">
|
||||
<h3 class="artifact-heading">Artifact Envelope</h3>
|
||||
<div class="artifact-grid">
|
||||
<div class="artifact-field"><span class="af-label">Product</span><span class="af-value">${this.esc(this.artifact.spec.product_type)}</span></div>
|
||||
<div class="artifact-field"><span class="af-label">Dimensions</span><span class="af-value">${this.artifact.spec.dimensions.width_mm}×${this.artifact.spec.dimensions.height_mm}mm</span></div>
|
||||
<div class="artifact-field"><span class="af-label">DPI</span><span class="af-value">${this.artifact.spec.dpi}</span></div>
|
||||
<div class="artifact-field"><span class="af-label">Color Space</span><span class="af-value">${this.artifact.spec.color_space}</span></div>
|
||||
<div class="artifact-field"><span class="af-label">Capabilities</span><span class="af-value">${(this.artifact.spec.required_capabilities || []).join(", ")}</span></div>
|
||||
<div class="artifact-field"><span class="af-label">Substrates</span><span class="af-value">${(this.artifact.spec.substrates || []).join(", ")}</span></div>
|
||||
</div>
|
||||
<div class="artifact-targets">
|
||||
${Object.values(this.artifact.render_targets).map((t: any) => `<span class="target-chip">${t.label}: ${t.dimensions}</span>`).join("")}
|
||||
</div>
|
||||
<div class="artifact-actions">
|
||||
${(this.artifact.next_actions || []).map((a: any) => `<span class="action-chip">${a.label}</span>`).join("")}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Provider match table
|
||||
html += `
|
||||
<div class="provider-section">
|
||||
<h3 class="provider-heading">Provider Matching <span class="buyer-loc">(buyer: Berlin)</span></h3>
|
||||
<div class="provider-table">
|
||||
<div class="pt-header">
|
||||
<span>Provider</span><span>Type</span><span>City</span><span>Distance</span><span>Cost</span><span>Turnaround</span>
|
||||
</div>
|
||||
${providers.map((prov, i) => `
|
||||
<div class="pt-row ${i === 0 ? 'nearest' : ''}">
|
||||
<span class="pt-name">${prov.name}</span>
|
||||
<span><span class="badge ${prov.type === 'cosmolocal' ? 'badge-cosmo' : 'badge-global'}">${prov.type}</span></span>
|
||||
<span>${prov.city}</span>
|
||||
<span>${prov.distance === Infinity ? '--' : `~${prov.distance.toLocaleString()} km`}</span>
|
||||
<span class="pt-cost">$${prov.unitCost.toFixed(2)}</span>
|
||||
<span>${prov.turnaround}</span>
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Revenue split bar
|
||||
html += `
|
||||
<div class="split-section">
|
||||
<h4 class="split-heading">Revenue Split <span class="split-total">(from $${unitCost.toFixed(2)} unit cost)</span></h4>
|
||||
<div class="split-bar">
|
||||
<div class="split-seg split-provider" style="flex:50">Provider 50% <span class="split-amt">$${provAmt}</span></div>
|
||||
<div class="split-seg split-creator" style="flex:35">Creator 35% <span class="split-amt">$${creatorAmt}</span></div>
|
||||
<div class="split-seg split-community" style="flex:15"><span class="split-amt">$${communityAmt}</span></div>
|
||||
</div>
|
||||
<div class="split-legend"><span>Community 15%</span></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `</section>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
private renderStep4(): string {
|
||||
return `
|
||||
<section class="step-section visible">
|
||||
<h3 class="pipeline-heading">Print-on-Demand Pipeline</h3>
|
||||
<div class="pipeline">
|
||||
<div class="pipe-node done"><div class="pipe-icon">${this.productIcon("tee")}</div><div class="pipe-label">rSwag Design</div><span class="pipe-check">✓</span></div>
|
||||
<div class="pipe-arrow">→</div>
|
||||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><rect x="2" y="4" width="16" height="12" rx="2" fill="none" stroke="#818cf8" stroke-width="1.5"/><line x1="6" y1="8" x2="14" y2="8" stroke="#818cf8" stroke-width="1"/><line x1="6" y1="11" x2="12" y2="11" stroke="#818cf8" stroke-width="1"/></svg></div><div class="pipe-label">Artifact Spec</div><span class="pipe-check">✓</span></div>
|
||||
<div class="pipe-arrow">→</div>
|
||||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><rect x="3" y="3" width="14" height="14" rx="3" fill="none" stroke="#818cf8" stroke-width="1.5"/><circle cx="10" cy="10" r="3" fill="#818cf8"/></svg></div><div class="pipe-label">rCart Catalog</div><span class="pipe-check">✓</span></div>
|
||||
<div class="pipe-arrow">→</div>
|
||||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><circle cx="10" cy="8" r="4" fill="none" stroke="#818cf8" stroke-width="1.5"/><path d="M4,18 Q4,13 10,13 Q16,13 16,18" fill="none" stroke="#818cf8" stroke-width="1.5"/></svg></div><div class="pipe-label">Provider Match</div><span class="pipe-check">✓</span></div>
|
||||
<div class="pipe-arrow">→</div>
|
||||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><rect x="3" y="5" width="14" height="10" rx="2" fill="none" stroke="#818cf8" stroke-width="1.5"/><path d="M3,8 L10,12 L17,8" fill="none" stroke="#818cf8" stroke-width="1.5"/></svg></div><div class="pipe-label">Local Print</div><span class="pipe-check">✓</span></div>
|
||||
</div>
|
||||
<div class="pipeline-actions">
|
||||
<a class="btn btn-primary" href="/demo/cart">Send to rCart</a>
|
||||
<button class="btn btn-secondary json-toggle-btn">View Artifact JSON</button>
|
||||
</div>
|
||||
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
private productIcon(id: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
tee: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M7,4 L4,6 L2,12 L5,13 L6,9 L6,22 L18,22 L18,9 L19,13 L22,12 L20,6 L17,4 L15,6 Q14,8 12,8 Q10,8 9,6 Z" fill="none" stroke="#818cf8" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
|
||||
sticker: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="#818cf8" stroke-width="1.5"/><circle cx="12" cy="12" r="5" fill="none" stroke="#818cf8" stroke-width="1.5"/></svg>`,
|
||||
poster: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="4" y="2" width="16" height="20" rx="2" fill="none" stroke="#818cf8" stroke-width="1.5"/><line x1="8" y1="7" x2="16" y2="7" stroke="#818cf8" stroke-width="1"/><line x1="8" y1="10" x2="16" y2="10" stroke="#818cf8" stroke-width="1"/><rect x="8" y="13" width="8" height="5" rx="1" fill="none" stroke="#818cf8" stroke-width="1"/></svg>`,
|
||||
hoodie: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M7,5 L3,7 L1,14 L4,15 L5,10 L5,22 L19,22 L19,10 L20,15 L23,14 L21,7 L17,5 L15,7 Q14,9 12,9 Q10,9 9,7 Z" fill="none" stroke="#818cf8" stroke-width="1.5" stroke-linejoin="round"/><ellipse cx="12" cy="6" rx="3" ry="2" fill="none" stroke="#818cf8" stroke-width="1"/></svg>`,
|
||||
};
|
||||
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 = `
|
||||
<style>
|
||||
:host { display: block; padding: 1.5rem; max-width: 900px; margin: 0 auto; }
|
||||
.products { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.products { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; }
|
||||
.product { padding: 1rem; 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); }
|
||||
|
|
@ -165,7 +609,7 @@ class FolkSwagDesigner extends HTMLElement {
|
|||
.json-pre.visible { display: block; }
|
||||
input[type="file"] { display: none; }
|
||||
@media (max-width: 768px) {
|
||||
.products { grid-template-columns: 1fr; }
|
||||
.products { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -182,33 +626,33 @@ class FolkSwagDesigner extends HTMLElement {
|
|||
<div class="upload-area ${this.imagePreview ? 'has-image' : ''}">
|
||||
${this.imagePreview
|
||||
? `<img class="preview-img" src="${this.imagePreview}" alt="Preview">`
|
||||
: `<div class="upload-label">📁 Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
|
||||
: `<div class="upload-label">Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
|
||||
<input type="file" accept="image/*">
|
||||
</div>
|
||||
|
||||
<input class="title-input" type="text" placeholder="Design title" value="${this.esc(this.designTitle)}">
|
||||
|
||||
<button class="generate-btn" ${(this.space !== "demo" && !this.imageFile) || this.generating ? 'disabled' : ''}>
|
||||
${this.generating ? '⏳ Generating...' : '🚀 Generate Print-Ready Files'}
|
||||
<button class="generate-btn" ${!this.imageFile || this.generating ? 'disabled' : ''}>
|
||||
${this.generating ? 'Generating...' : 'Generate Print-Ready Files'}
|
||||
</button>
|
||||
|
||||
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
||||
|
||||
${this.artifact ? `
|
||||
<div class="result">
|
||||
<h3 class="result-title">✅ ${this.esc(this.artifact.payload?.title || "Artifact")}</h3>
|
||||
<h3 class="result-title">${this.esc(this.artifact.payload?.title || this.artifact.title || "Artifact")}</h3>
|
||||
<div class="result-meta">
|
||||
${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
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => `
|
||||
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank">⬇ Download ${target.format.toUpperCase()}</a>
|
||||
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank">Download ${target.format.toUpperCase()}</a>
|
||||
`).join("")}
|
||||
<button class="result-btn result-btn-secondary" data-action="copy-json">📋 Copy Artifact JSON</button>
|
||||
<button class="result-btn result-btn-secondary" data-action="copy-json">Copy Artifact JSON</button>
|
||||
</div>
|
||||
<span class="json-toggle">Show artifact envelope ▼</span>
|
||||
<span class="json-toggle">Show artifact envelope</span>
|
||||
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
|
||||
</div>` : ""}
|
||||
`;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,81 @@ export function renderLanding(): string {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Global, Manufacture Local -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Design Global, Manufacture Local</h2>
|
||||
<p class="rl-subtext" style="text-align:center">One artifact spec powers the entire pipeline — from design to local fulfillment.</p>
|
||||
<div class="rl-grid-5" style="display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;margin-top:2rem;">
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">1</div>
|
||||
<h3>Design in rSwag</h3>
|
||||
<p>Pick a product, upload artwork, and configure sizes and colors.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">2</div>
|
||||
<h3>Generate Artifact</h3>
|
||||
<p>Print-ready files with specs, DPI, bleed, color profiles, and metadata.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">3</div>
|
||||
<h3>List on rCart</h3>
|
||||
<p>The artifact becomes a catalog product, ready for community ordering.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">4</div>
|
||||
<h3>Match Provider</h3>
|
||||
<p>Nearest capable local print shop gets the job. Printful as global fallback.</p>
|
||||
</div>
|
||||
<div class="rl-step">
|
||||
<div class="rl-step__num">5</div>
|
||||
<h3>Print & Ship Locally</h3>
|
||||
<p>Community printer fulfills the order. Revenue auto-splits to all parties.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rl-card" style="margin-top:1.5rem;text-align:center;border-color:#334155;">
|
||||
<p style="color:#94a3b8;font-size:0.875rem;margin:0;">The same artifact envelope that powers pocket book publishing also drives swag fulfillment. <strong style="color:#e2e8f0;">One spec, many products.</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Local + Global Fulfillment -->
|
||||
<section class="rl-section rl-section--alt">
|
||||
<div class="rl-container">
|
||||
<h2 class="rl-heading" style="text-align:center">Local + Global Fulfillment</h2>
|
||||
<p class="rl-subtext" style="text-align:center">Community printers first, global POD as fallback. Always fulfilled.</p>
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:1.5rem;margin-top:2rem;">
|
||||
<div class="rl-card">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
|
||||
<span style="background:rgba(34,197,94,0.15);color:#4ade80;padding:0.25rem 0.625rem;border-radius:999px;font-size:0.75rem;font-weight:600;">cosmolocal</span>
|
||||
<h3 style="margin:0;font-size:1rem;">Community Print Network</h3>
|
||||
</div>
|
||||
<p>6 community print shops across 4 continents. Matched by proximity and capability. Lower cost, faster delivery, supports the local economy.</p>
|
||||
<p style="color:#64748b;font-size:0.8125rem;margin-bottom:0;">DTG, screen print, vinyl cut, risograph, inkjet — capabilities vary by shop.</p>
|
||||
</div>
|
||||
<div class="rl-card">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.75rem;">
|
||||
<span style="background:rgba(56,189,248,0.15);color:#38bdf8;padding:0.25rem 0.625rem;border-radius:999px;font-size:0.75rem;font-weight:600;">global</span>
|
||||
<h3 style="margin:0;font-size:1rem;">Printful (Global Fallback)</h3>
|
||||
</div>
|
||||
<p>DTG apparel, vinyl stickers, and art prints shipped worldwide. Always available. Bella+Canvas blanks. Sandbox mode for testing.</p>
|
||||
<p style="color:#64748b;font-size:0.8125rem;margin-bottom:0;">SKU 71 (tee), SKU 146 (hoodie), SKU 358 (sticker) — full size and color ranges.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:1.5rem;">
|
||||
<p style="text-align:center;color:#94a3b8;font-size:0.8125rem;margin-bottom:0.5rem;">Revenue Split on Every Order</p>
|
||||
<div style="display:flex;height:32px;border-radius:8px;overflow:hidden;font-size:0.75rem;font-weight:600;">
|
||||
<div style="flex:50;background:#16a34a;color:#fff;display:flex;align-items:center;justify-content:center;">Provider 50%</div>
|
||||
<div style="flex:35;background:#4f46e5;color:#fff;display:flex;align-items:center;justify-content:center;">Creator 35%</div>
|
||||
<div style="flex:15;background:#d97706;color:#fff;display:flex;align-items:center;justify-content:center;">15%</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:0.25rem;">
|
||||
<span style="color:#64748b;font-size:0.6875rem;">Community 15%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Products (module-specific) -->
|
||||
<section class="rl-section">
|
||||
<div class="rl-container">
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, ProductTemplate> = {
|
|||
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<string, ProductTemplate> = {
|
|||
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" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue