198 lines
8.3 KiB
TypeScript
198 lines
8.3 KiB
TypeScript
/**
|
|
* <folk-swag-designer> — 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 selectedProduct = "sticker";
|
|
private imageFile: File | null = null;
|
|
private imagePreview = "";
|
|
private title = "";
|
|
private generating = false;
|
|
private artifact: any = null;
|
|
private error = "";
|
|
|
|
constructor() {
|
|
super();
|
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
}
|
|
|
|
connectedCallback() {
|
|
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.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.title || "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: "\u{1F4CB}", desc: "A4 vinyl stickers" },
|
|
{ id: "poster", name: "Poster (A3)", icon: "\u{1F5BC}", desc: "A3 art print" },
|
|
{ id: "tee", name: "T-Shirt", icon: "\u{1F455}", desc: "12x16\" DTG print" },
|
|
];
|
|
|
|
this.shadow.innerHTML = `
|
|
<style>
|
|
:host { display: block; padding: 1.5rem; max-width: 900px; margin: 0 auto; }
|
|
h2 { color: #f1f5f9; margin: 0 0 1.5rem; font-size: 1.5rem; }
|
|
.products { display: grid; grid-template-columns: repeat(3, 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); }
|
|
.product-icon { font-size: 2rem; margin-bottom: 0.375rem; }
|
|
.product-name { color: #f1f5f9; font-weight: 600; font-size: 0.875rem; }
|
|
.product-desc { color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; }
|
|
.upload-area { border: 2px dashed #334155; border-radius: 12px; padding: 2rem; text-align: center; margin-bottom: 1rem; cursor: pointer; transition: border-color 0.15s; background: #1e293b; }
|
|
.upload-area:hover { border-color: #6366f1; }
|
|
.upload-area.has-image { border-style: solid; border-color: #475569; }
|
|
.upload-label { color: #94a3b8; font-size: 0.875rem; }
|
|
.preview-img { max-width: 200px; max-height: 200px; border-radius: 8px; }
|
|
.title-input { width: 100%; padding: 0.625rem 0.75rem; border: 1px solid #334155; border-radius: 8px; background: #1e293b; color: #f1f5f9; font-size: 0.875rem; margin-bottom: 1rem; box-sizing: border-box; }
|
|
.title-input:focus { outline: none; border-color: #6366f1; }
|
|
.generate-btn { width: 100%; padding: 0.75rem; border: none; border-radius: 8px; background: #4f46e5; color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer; margin-bottom: 1rem; }
|
|
.generate-btn:hover { background: #4338ca; }
|
|
.generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.error { background: rgba(239,68,68,0.1); border: 1px solid #ef4444; border-radius: 8px; padding: 0.75rem; color: #fca5a5; font-size: 0.875rem; margin-bottom: 1rem; }
|
|
.result { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.25rem; }
|
|
.result-title { color: #f1f5f9; font-weight: 600; margin: 0 0 0.5rem; }
|
|
.result-meta { color: #94a3b8; font-size: 0.8125rem; margin-bottom: 1rem; }
|
|
.result-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
|
.result-btn { padding: 0.5rem 1rem; border-radius: 8px; font-size: 0.875rem; text-decoration: none; font-weight: 500; cursor: pointer; border: none; }
|
|
.result-btn-primary { background: #4f46e5; color: #fff; }
|
|
.result-btn-primary:hover { background: #4338ca; }
|
|
.result-btn-secondary { background: #334155; color: #f1f5f9; }
|
|
.result-btn-secondary:hover { background: #475569; }
|
|
.json-toggle { color: #818cf8; cursor: pointer; font-size: 0.75rem; margin-top: 0.75rem; display: inline-block; }
|
|
.json-pre { background: #0f172a; border-radius: 8px; padding: 0.75rem; overflow-x: auto; font-size: 0.6875rem; color: #94a3b8; margin-top: 0.5rem; max-height: 300px; display: none; }
|
|
.json-pre.visible { display: block; }
|
|
input[type="file"] { display: none; }
|
|
</style>
|
|
|
|
<h2>\u{1F3A8} Swag Designer</h2>
|
|
|
|
<div class="products">
|
|
${products.map((p) => `
|
|
<div class="product ${this.selectedProduct === p.id ? 'active' : ''}" data-product="${p.id}">
|
|
<div class="product-icon">${p.icon}</div>
|
|
<div class="product-name">${p.name}</div>
|
|
<div class="product-desc">${p.desc}</div>
|
|
</div>
|
|
`).join("")}
|
|
</div>
|
|
|
|
<div class="upload-area ${this.imagePreview ? 'has-image' : ''}">
|
|
${this.imagePreview
|
|
? `<img class="preview-img" src="${this.imagePreview}" alt="Preview">`
|
|
: `<div class="upload-label">\u{1F4C1} 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.title)}">
|
|
|
|
<button class="generate-btn" ${!this.imageFile || this.generating ? 'disabled' : ''}>
|
|
${this.generating ? '\u23F3 Generating...' : '\u{1F680} Generate Print-Ready Files'}
|
|
</button>
|
|
|
|
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
|
|
|
${this.artifact ? `
|
|
<div class="result">
|
|
<h3 class="result-title">\u2705 ${this.esc(this.artifact.payload?.title || "Artifact")}</h3>
|
|
<div class="result-meta">
|
|
${this.esc(this.artifact.spec?.product_type || "")} \u2022
|
|
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm \u2022
|
|
${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">\u{2B07} Download ${target.format.toUpperCase()}</a>
|
|
`).join("")}
|
|
<button class="result-btn result-btn-secondary" data-action="copy-json">\u{1F4CB} Copy Artifact JSON</button>
|
|
</div>
|
|
<span class="json-toggle">Show artifact envelope \u25BC</span>
|
|
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
|
|
</div>` : ""}
|
|
`;
|
|
|
|
// 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.title = (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);
|