// Top-level routed app. Wraps the existing single-page App in a // router + TemplateStoreProvider so admin + future shop routes can // live alongside the editor. // // v0.8 routes // ─────────── // /auth/login → LoginRoute (auth-manager) // /admin/login → AdminLoginPage (auth-manager) // /account → AccountShell (auth-manager) // /account/addresses → AccountAddressesPage (auth-manager) // /admin/inventory → InventoryListPanel // /admin/inventory/:id → InventoryDetailPanel // (admin routes are now wrapped in from auth-manager) // (other routes unchanged from v0.7) // // v0.7 routes // ─────────── // / → ShopHome (replaces legacy App.jsx) // /shop → ShopHome (alias) // /shop/:productSlug → ProductPage // /cart → CartStub // /customize → CustomizeRoute // /admin/templates → TemplateListPanel + CreateTemplateModal // /admin/templates/:id → TemplateEditorPanel // /admin/products → ProductListPanel // /admin/products/:id → ProductEditorPanel // /admin/variants → VariantMockupPanel (retired stub) // /admin/featured → FeaturedOrderPanel // /admin/trash → TemplateTrashPanel // * → ShopHome (SPA fallback) // // v0.8 changes // ──────────── // • Inventory admin surfaces (list + detail) wired in for the new // material-kind catalog. The list reads ?productId from window // location for deep-linking from a material product's editor. // • Per-option pricing rolls up into ProductBuyPanel's unit price; // no routing changes — the buy panel sums priceCents internally. // • Auth surface (auth-manager) mounts at /auth/login, /admin/login, // and /account/*. Admin routes are now wrapped in // for the client guard; server-side, the API endpoints still need // requireAdmin middleware once template-manager + commerce-module // wire it in. // // v0.7 changes // ──────────── // • Root `/` now serves the shop home, not the legacy App.jsx. // App.jsx stays on disk one release for safety but is no longer // routed. Existing /customize?template= deep links continue // to work via CustomizeRoute. // • /shop/:productSlug mounts the customer-facing product detail. // Both customizable and fixed kinds render here; the buy panel // forks on kind internally. // • /cart shows a localStorage-backed cart preview. "Proceed to // checkout" surfaces "Checkout coming soon" — checkout is // downstream. // // v0.6 changes (retained context) // ─────────────────────────────── // • CreateTemplateModal pulls products from the API now. // • AdminEditorCanvas reads template.variant directly. // • TemplateStoreProvider no longer receives variantCatalog or // phoneModels — template-manager owns the catalog. import { useMemo, useState, useCallback } from 'react'; import { BrowserRouter, Routes, Route, Link, useNavigate, useParams, useLocation } from 'react-router-dom'; import { ApparelDesigner, dematerializeTemplate, DEFAULT_STICKER_CATEGORIES } from 'goods-editor'; import 'goods-editor/styles.css'; import { TemplateStoreProvider, TemplateListPanel, TemplateEditorPanel, TemplateTrashPanel, VariantMockupPanel, FeaturedOrderPanel, CreateTemplateModal, ProductListPanel, ProductEditorPanel, CreateProductModal, // v0.8 — inventory admin panels. InventoryListPanel, InventoryDetailPanel, useAdminTemplates, useAdminProducts, // Facet personalization (Project doc §R10). FacetValueManager, } from 'template-manager'; import 'template-manager/styles.css'; // v0.8 — auth surface. import { LoginForm, AdminLoginPage, RequireAdmin, AccountShell, AccountHomePage, AccountAddressesPage, // Facet personalization (Project doc §R10). FacetPrompt, AccountFacetPage, useFacetConfig, } from '@khalidadil/auth-manager'; import './styles/admin.css'; import './styles/shop.css'; import CustomizeRoute from './CustomizeRoute'; import { ShopHome } from './components/shop/ShopHome'; import { ProductPage } from './components/shop/ProductPage'; import { CartStub } from './components/shop/CartStub'; import { DEFAULT_PRODUCT_ID, DEFAULT_SIZE_ID } from './constants/seedProducts'; import { SHIRT_COLORS, SHIRT_SIZES, SHIRT_BASE_PRICE_USD } from './constants/shirt'; import { PHONE_MODELS } from './constants/phoneModels'; // URL builders — single source of truth for the URL shape so the // rest of the app uses these instead of hand-building strings. const customizeUrl = (templateId) => templateId ? `/customize?template=${encodeURIComponent(templateId)}` : '/customize'; const adminTemplateUrl = (templateId) => `/admin/templates/${encodeURIComponent(templateId)}`; const adminTemplateListUrl = '/admin/templates'; const adminProductListUrl = '/admin/products'; const adminProductUrl = (productId) => `/admin/products/${encodeURIComponent(productId)}`; // v0.8 — inventory admin URLs. const adminInventoryListUrl = '/admin/inventory'; const adminInventoryUrl = (variantId) => `/admin/inventory/${encodeURIComponent(variantId)}`; // v0.6 — shop URL now keyed by product slug (or id; both work since // public template GET embeds the product). Existing shop pages keyed // by productTypeId continue to work because legacy slugs match the // new product slugs ("shirt", "magnet", …). const shopTemplateUrl = (productSlug, templateId) => `/shop/${encodeURIComponent(productSlug)}/${encodeURIComponent(templateId)}`; function AdminShell({ children }) { // Facet personalization (Project doc §R10) — show the manager link // only when a facet is configured; label comes from the config. const { config: facetConfig } = useFacetConfig(); return (
{children}
); } // ── AdminEditorCanvas — host's adapter for TemplateEditorPanel's // `renderCanvas` prop. // // v0.6 changes // ──────────── // The variant used to come from getVariant(productTypeId, variantId) // against the host's PRODUCT_TYPES. Now it comes from the template // itself — the templates GET response embeds a `variant` object // resolved server-side via productsStore. We just read it. function AdminEditorCanvas({ metadata, version, updateVersion }) { // metadata.variant is set by useTemplateDraft from the GET // response (the route embeds product/size/variant). When a brand- // new template has no productId yet (edge case during create), // variant is null; we still mount the editor with a sensible // default canvas size. const variant = metadata?.variant || null; // Marketing Gallery Entry (Round 6) — persist Marketing-tab edits to the // product-level entryConfig. `entryConfigOverride` holds the admin's // optimistic edits for THIS session so the editor's canvas showcase + the // Marketing-tab Play preview reflect changes live; until they edit, the // effective config is the server value resolved onto the variant. The // dedicated PATCH persists it (productId comes from the resolved variant). const { patchEntryConfig } = useAdminProducts(); const productId = variant?.productId || null; const [entryConfigOverride, setEntryConfigOverride] = useState(null); const effectiveEntryConfig = entryConfigOverride !== null ? entryConfigOverride : (variant?.entryConfig || null); const handleEntryConfigChange = useCallback((next) => { setEntryConfigOverride(next); // optimistic — keeps the canvas + Play preview in sync with the tab if (!productId) return; patchEntryConfig(productId, next).catch((err) => { console.error('[admin editor] entry-config save failed:', err.message); }); }, [patchEntryConfig, productId]); const template = useMemo(() => { if (!metadata) return null; // Multi-view (2026-06-10, schema v4): the version is a `views` // map; the editor's materializer reads only that. Templates are // currently authored single-view here (no `views` prop is passed // to ApparelDesigner below), so the editor synthesizes its // 'default' view and the map round-trips as { default: {…} }. return { ...metadata, version: version ? { views: version.views || {}, schemaVersion: version.schemaVersion ?? 4, } : null, }; }, [metadata, version]); // Multi-view (2026-06-10) — convert absolute pixel coords back to // fractional on the way out. onViewsChange replaces the old // onElementsChange write-through: it fires at the same commit // boundaries but carries the FULL views map, so the draft always // captures every view (today that's just 'default'; when this // canvas grows a `views` prop nothing here changes). // dematerializeTemplate's flat→default wrap is NOT used here — the // editor hands us a map already. const canvasSize = variant?.canvas?.width || 300; const handleViewsChange = useCallback((viewsAbsMap) => { const fractional = dematerializeTemplate({ views: viewsAbsMap, canvasSize, }); updateVersion({ views: fractional.views, schemaVersion: fractional.schemaVersion, }); }, [updateVersion, canvasSize]); // Upload handler (2026-06-09 — slot-preview persistence). Mirrors // CustomizeRoute's handler (POST /api/upload → server-hosted URL), // minus the toast surface (admin shell has none here; failures log // and rethrow — the editor's upload paths degrade gracefully). // // Why the admin canvas needs this: goods-editor's admin slot-preview // pipeline persists el.slot.previewSrc ONLY when onUploadFile is // wired — otherwise previews stay an in-session blob-URL overlay // that vanishes on reload and never reaches the stored template, so // the customer-side faded preview placeholder silently never works. // It also keeps the admin Upload tab on server URLs instead of the // FileReader data: fallback, which would otherwise dematerialize // multi-MB base64 strings into the template version JSON. const handleUploadFile = useCallback(async (file) => { const formData = new FormData(); formData.append('image', file); const response = await fetch('/api/upload', { method: 'POST', body: formData }); if (!response.ok) { const errorText = await response.text().catch(() => ''); const message = `Upload failed (${response.status}${errorText ? `: ${errorText}` : ''})`; console.error('[admin editor]', message); throw new Error(message); } const data = await response.json(); return { url: data.preview.url, originalUrl: data.original.url }; }, []); if (!template) { return (

Loading template metadata…

); } return (
); } // ── /admin/templates — list + create flow ────────────────────── function AdminTemplateListRoute() { const navigate = useNavigate(); const { createTemplate } = useAdminTemplates(); const [createOpen, setCreateOpen] = useState(false); const handleCreate = async (payload) => { const created = await createTemplate(payload); setCreateOpen(false); navigate(adminTemplateUrl(created.id)); return created; }; return ( setCreateOpen(true)} /> {createOpen && ( setCreateOpen(false)} /> )} ); } // ── /admin/templates/:id — editor ────────────────────────────── function AdminTemplateEditorRoute() { const { id } = useParams(); return ( } onMarketingRegenerate={async (templateId) => { try { const res = await fetch( `/api/templates/${encodeURIComponent(templateId)}/marketing-preview/regenerate`, { method: 'POST' }, ); if (!res.ok) { const body = await res.json().catch(() => ({})); console.error('Regenerate failed:', body.error || res.status); } } catch (err) { console.error('Regenerate request failed:', err); } }} /> ); } // ── /admin/products — list + create flow ─────────────────────── function AdminProductListRoute() { const navigate = useNavigate(); const { createProduct } = useAdminProducts(); const [createOpen, setCreateOpen] = useState(false); // Two-step create: POST product, then optionally POST first size. // We POST the size via a raw fetch rather than wiring useProduct // here — the modal returns to the parent before the product page // is mounted, so there's no useProduct instance yet. A direct fetch // is simpler than coordinating the timing. const handleCreate = async ({ product, firstSize }) => { const created = await createProduct(product); if (firstSize) { try { const res = await fetch(`/api/products/${encodeURIComponent(created.id)}/sizes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(firstSize), }); if (!res.ok) { const body = await res.json().catch(() => ({})); // Don't fail the whole flow — the product was created; // just surface the size error in the console. Admin will // see "no sizes" on the product page and can add one. console.warn('First-size create failed:', body.error || res.status); } } catch (err) { console.warn('First-size create network error:', err.message); } } setCreateOpen(false); navigate(adminProductUrl(created.id)); return created; }; return ( setCreateOpen(true)} /> {createOpen && ( setCreateOpen(false)} /> )} ); } // ── /admin/products/:id — editor ─────────────────────────────── function AdminProductEditorRoute() { const { id } = useParams(); return ( ); } // v0.8 — /admin/inventory — list. // // InventoryListPanel reads ?productId from window.location.search on // mount, so the "View inventory variants →" jump-link inside the // material-product editor deep-links into a pre-filtered list. No // extra plumbing needed here. function AdminInventoryListRoute() { return ( ); } // v0.8 — /admin/inventory/:id — single-variant detail. function AdminInventoryDetailRoute() { const { id } = useParams(); return ( ); } // Facet personalization (Project doc §R10) — /admin/facets — manage the // facet vocabulary + the sticker-category→value assignment map. Sticker // categories come from goods-editor's DEFAULT_STICKER_CATEGORIES. function AdminFacetRoute() { return ( ); } // v0.8 — small wrapper to pull `next` out of the URL search before // handing it to LoginForm. LoginForm itself stays prop-driven so it // can be reused in non-router contexts later. function LoginRoute() { const location = useLocation(); const params = new URLSearchParams(location.search); const next = params.get('next') || undefined; return (
); } export default function AppRouter() { return ( {/* Facet personalization (Project doc §R10) — one-time dismissable sign-in nudge for customers with no selection. Renders nothing when no facet is configured / customer signed out / already chosen. Inside AuthProvider (from main.jsx) so it sees auth. */} } /> } /> } /> } /> } /> {/* v0.8 — auth + account */} } /> } /> }> } /> } /> {/* Facet personalization (Project doc §R10) — AccountFacetPage hides itself when no facet is configured. */} } /> {/* /account/designs lands once goods-editor-module exports a MyDesignsPage component (Phase 2 / v08_GoodsEditor_v1). /account/orders lands with commerce-module. */} {/* Admin — wrapped in RequireAdmin (client guard). Server- side, the API endpoints still need requireAdmin middleware once template-manager + commerce-module wire it in. */}
} />
} /> } /> } /> {/* v0.8 — inventory routes. */} } /> } /> {/* Facet personalization (Project doc §R10). */} } /> } /> } /> } /> {/* SPA fallback — unknown routes land on the shop home. */} } /> ); }