/** * — upload artwork → generate print-ready files. * Product selector (sticker, poster, tee), image upload with preview, * generate button, artifact result display with download link. */ class FolkSwagDesigner extends HTMLElement { private shadow: ShadowRoot; private space = ""; private selectedProduct = "sticker"; private imageFile: File | null = null; private imagePreview = ""; private designTitle = ""; private generating = false; private artifact: any = null; private error = ""; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || ""; if (this.space === "demo") { this.loadDemoData(); 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 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); return; } if (!this.imageFile || this.generating) return; this.generating = true; this.error = ""; this.artifact = null; this.render(); try { const formData = new FormData(); formData.append("image", this.imageFile); formData.append("product", this.selectedProduct); formData.append("title", this.designTitle || "Untitled Design"); const res = await fetch(`${this.getApiBase()}/api/artifact`, { method: "POST", body: formData, }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || `Failed: ${res.status}`); } this.artifact = await res.json(); } catch (e) { this.error = e instanceof Error ? e.message : "Generation failed"; } finally { this.generating = false; this.render(); } } private render() { 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" }, ]; this.shadow.innerHTML = `
${products.map((p) => `
${p.icon}
${p.name}
${p.desc}
`).join("")}
${this.imagePreview ? `Preview` : `
📁 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.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()} `).join("")}
Show artifact envelope ▼
${this.esc(JSON.stringify(this.artifact, null, 2))}
` : ""} `; // Event listeners this.shadow.querySelectorAll(".product").forEach((el) => { el.addEventListener("click", () => { this.selectedProduct = (el as HTMLElement).dataset.product || "sticker"; this.render(); }); }); const uploadArea = this.shadow.querySelector(".upload-area"); const fileInput = this.shadow.querySelector('input[type="file"]') as HTMLInputElement; uploadArea?.addEventListener("click", () => fileInput?.click()); fileInput?.addEventListener("change", () => { const file = fileInput.files?.[0]; if (file) { this.imageFile = file; this.imagePreview = URL.createObjectURL(file); this.render(); } }); this.shadow.querySelector(".title-input")?.addEventListener("input", (e) => { this.designTitle = (e.target as HTMLInputElement).value; }); this.shadow.querySelector(".generate-btn")?.addEventListener("click", () => this.generate()); this.shadow.querySelector(".json-toggle")?.addEventListener("click", () => { const pre = this.shadow.querySelector(".json-pre"); pre?.classList.toggle("visible"); }); this.shadow.querySelector('[data-action="copy-json"]')?.addEventListener("click", () => { navigator.clipboard.writeText(JSON.stringify(this.artifact, null, 2)); }); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } } customElements.define("folk-swag-designer", FolkSwagDesigner);