import React, { useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "wouter"; import { ArrowLeft, Download } from "lucide-react"; import { Link } from "wouter"; import { DashboardTemplatePage } from "@/components/DashboardTemplatePage"; import type { LogoTemplate as FirebaseLogoDoc } from "../types/logo-types"; // --- helpers --------------------------------------------------------------- function escapeRegExp(s: string) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function escapeXml(unsafe: string) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function triggerDownload(url: string, filename: string) { const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } function ensureSvgRoot(svgString: string): string { if (!svgString) return ""; let s = svgString.trim(); s = s.replace(/^\s*<\?xml[^>]*>\s*/i, ""); s = s.replace(/]*>/gi, ""); if (!/]/i.test(s)) { s = `${s}`; } s = s.replace( /]*\sxmlns=)/i, ``); } if (!/role="/i.test(s)) { s = s.replace(/]*)>/i, ``); } return s; } // quick arc path const buildArcPath = (x: number, y: number, width: number, curve: number) => { const x0 = x - width / 2; const x1 = x + width / 2; const arcFlag = curve > 0 ? 0 : 1; const rx = Math.abs(curve) + width / 2; return `M ${x0},${y} A ${rx},${rx} 0 0,${arcFlag} ${x1},${y}`; }; // --- component ------------------------------------------------------------- export default function LogoCustomizerPage() { const { id } = useParams<{ id: string }>(); // template const [template, setTemplate] = useState<{ id: string; name: string; svg: string; // raw svg (may be empty if cors blocked) previewUrl?: string; defaultFields?: { Brand_Name?: string; Tagline?: string; Est_Year?: string }; fieldOrder?: string[]; defaultColors?: { primary: string; secondary?: string; accent?: string }; } | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // form state const [brandName, setBrandName] = useState("Your Brand"); const [tagline, setTagline] = useState("Your Tagline"); const [estYear, setEstYear] = useState("2025"); const [colors, setColors] = useState<{ primary: string; secondary: string; accent: string }>({ primary: "#231f20", secondary: "#978752", accent: "#6dc282", }); // transforms const [brandNameSize, setBrandNameSize] = useState(100); const [taglineSize, setTaglineSize] = useState(100); const [estYearSize, setEstYearSize] = useState(100); const [textPositions, setTextPositions] = useState>({}); const [textShapes, setTextShapes] = useState>({ "brand-name": { rotation: 0, curve: 0 }, "tagline": { rotation: 0, curve: 0 }, "est-year": { rotation: 0, curve: 0 }, }); const [dragId, setDragId] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const previewRef = useRef(null); // load template (firebase → proxy → manifest) useEffect(() => { if (!id) return; const load = async () => { setLoading(true); setError(null); // local util: fetch text (with friendlier errors) const fetchText = async (u: string) => { try { const r = await fetch(u, { cache: "no-store" }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return await r.text(); } catch (e: any) { throw new Error(`Failed to fetch ${u.split("?")[0]} (${e?.message || "network"})`); } }; try { // 1) try firebase try { const svc = await import("@/services/firebase-templates"); let fb: any = null; try { fb = await svc.getTemplateById(id); } catch {} if (!fb && id.startsWith("logo-wordmark-")) { try { fb = await svc.getTemplateById(id.replace("logo-wordmark-", "")); } catch {} } if (fb) { const out = { id: fb.id, name: fb.name || id, svg: "", previewUrl: fb.assets?.previewDownloadUrl, defaultFields: { Brand_Name: fb.defaults?.brandName || "Your Brand", Tagline: fb.defaults?.tagline || "Your Tagline", Est_Year: fb.defaults?.estYear || "2025", }, fieldOrder: ["Brand_Name", "Tagline", "Est_Year"], defaultColors: { primary: "#231f20", secondary: "#978752", accent: "#6dc282" }, }; // a) direct doc (preferred) try { const detail = await svc.readDoc("logo_templates", fb.id); if (detail?.svg) out.svg = detail.svg; } catch {} // b) proxy (cors-safe) if (!out.svg && fb.assets?.svgDownloadUrl) { try { const prox = `/api/firebase-proxy?url=${encodeURIComponent(fb.assets.svgDownloadUrl)}`; out.svg = await fetchText(prox); } catch {} } // finalize out.svg = ensureSvgRoot(out.svg); setTemplate(out); setBrandName(out.defaultFields!.Brand_Name!); setTagline(out.defaultFields!.Tagline!); setEstYear(out.defaultFields!.Est_Year!); setColors(out.defaultColors!); setLoading(false); return; } } catch (_) { // ignore, fall through to manifest } // 2) manifest fallback const manRes = await fetch("/site/data/manifest.logo.json", { cache: "no-cache" }); if (!manRes.ok) throw new Error("Failed to load template manifest"); const manifest = await manRes.json(); const item = manifest.items?.find((x: any) => x.id === id); if (!item) throw new Error(`Template ${id} not found`); let svg = ""; try { svg = await fetchText(item.svgUrl); } catch (e) { // leave blank; we will show previewUrl instead } const out = { id: item.id, name: item.name || id, svg: ensureSvgRoot(svg), previewUrl: item.previewUrl, defaultFields: item.defaultFields || { Brand_Name: "Your Brand", Tagline: "Your Tagline", Est_Year: "2025" }, fieldOrder: item.fieldOrder || ["Brand_Name", "Tagline", "Est_Year"], defaultColors: item.defaultColors || { primary: "#231f20", secondary: "#978752", accent: "#6dc282" }, }; setTemplate(out); setBrandName(out.defaultFields!.Brand_Name!); setTagline(out.defaultFields!.Tagline!); setEstYear(out.defaultFields!.Est_Year!); setColors(out.defaultColors!); } catch (e: any) { setError(e?.message || "Failed to load template"); } finally { setLoading(false); } }; load(); }, [id]); // build live svg (token replacement + CSS var injection + text transforms) const resolvedSvg = useMemo(() => { if (!template?.svg) return ""; // 1) replace tokens const vals = { Brand_Name: brandName, Tagline: tagline, Est_Year: estYear, }; let svg = template.svg; Object.entries(vals).forEach(([k, v]) => { const token = new RegExp(`\\{${escapeRegExp(k)}\\}`, "g"); svg = svg.replace(token, escapeXml(String(v))); }); // 2) inject colors on root svg = svg.replace( /]*?)>/, `` ); // 3) adjust text sizes/positions/curves/rotation by rewriting nodes let defs = ""; svg = svg.replace(/]*?)>([\s\S]*?)<\/text>/g, (m, attrs, content) => { // decode entities for matching const ta = document.createElement("textarea"); ta.innerHTML = content; const decoded = (ta.value || "").trim(); let idGuess = ""; let sizePct = 100; if (decoded && brandName && (decoded === brandName || brandName.includes(decoded) || decoded.includes(brandName))) { idGuess = "brand-name"; sizePct = brandNameSize; } else if (decoded && tagline && (decoded === tagline || tagline.includes(decoded) || decoded.includes(tagline))) { idGuess = "tagline"; sizePct = taglineSize; } else if (decoded && estYear && (decoded === estYear || estYear.includes(decoded) || decoded.includes(estYear))) { idGuess = "est-year"; sizePct = estYearSize; } // base font-size const fs = (attrs.match(/font-size="([^"]+)"/)?.[1] ?? "14").trim(); const base = parseFloat(fs) || 14; const scaled = base * (sizePct / 100); // base position let x = parseFloat(attrs.match(/x="([^"]+)"/)?.[1] ?? "0"); let y = parseFloat(attrs.match(/y="([^"]+)"/)?.[1] ?? "0"); if (idGuess && textPositions[idGuess]) { x = textPositions[idGuess].x; y = textPositions[idGuess].y; } const shape = idGuess ? textShapes[idGuess] : { rotation: 0, curve: 0 }; let newAttrs = attrs .replace(/\s*font-size="[^"]*"/g, "") .replace(/\s*x="[^"]*"/g, "") .replace(/\s*y="[^"]*"/g, "") .replace(/\s*transform="[^"]*"/g, "") .replace(/\s*id="[^"]*"/g, ""); if (shape?.curve) { const textWidth = decoded.length * scaled * 0.6; const arcId = `arc-${idGuess || "txt"}-${Math.random().toString(36).slice(2, 7)}`; defs += ``; newAttrs += ` id="${idGuess}" x="${x}" y="${y}" font-size="${scaled}" style="cursor:move;user-select:none"`; return `${content}`; } else { const rot = shape?.rotation || 0; const transform = rot ? ` transform="rotate(${rot} ${x} ${y})"` : ""; newAttrs += ` id="${idGuess}" x="${x}" y="${y}" font-size="${scaled}" style="cursor:move;user-select:none"`; return `${content}`; } }); if (defs) { svg = svg.replace(/(]*>)/, `$1${defs}`); } return svg; }, [template, brandName, tagline, estYear, colors, brandNameSize, taglineSize, estYearSize, textPositions, textShapes]); // drag handlers on the injected SVG useEffect(() => { const containe