v0.7: shop storefront + routed app
AppRouter replaces the single-page App as the mounted root: shop routes (/ and /shop → ShopHome, /shop/:productSlug → ProductPage, /cart → CartStub, /customize → CustomizeRoute) plus the template-manager admin surfaces (/admin/templates|products|featured|trash, v0.8 adds /admin/inventory + RequireAdmin wrapping). App.jsx stays on disk one release as the documented legacy fallback — nothing imports it. New shop UI: components/shop/ (ShopHome/Hero/ProductGrid/ProductCard/ ProductPage/ProductGallery/ProductInfo/ProductBuyPanel/CartStub/ ShopHeader/TemplateGalleryForProduct), hooks (useShopProducts/ useShopProduct/useCart/useFacetCatalog), and shop.css + admin.css. Header gains the module-gated Clear button (onCanClearChange / onElementCountChange mirror the editor module's history-commit signals); i18n keys follow. vite proxies /products to the Node server (template-manager serves product imagery — without it the SPA fallback returns index.html as an image) and adds a template-manager chunk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
519
src/AppRouter.jsx
Normal file
519
src/AppRouter.jsx
Normal file
@@ -0,0 +1,519 @@
|
||||
// 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 <RequireAdmin> 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 <RequireAdmin>
|
||||
// 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=<id> 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 (
|
||||
<div className="admin-shell">
|
||||
<nav className="admin-shell__nav">
|
||||
<div className="admin-shell__brand">Template Admin</div>
|
||||
<div className="admin-shell__links">
|
||||
<Link to={adminProductListUrl} className="admin-shell__link">Products</Link>
|
||||
<Link to={adminTemplateListUrl} className="admin-shell__link">Templates</Link>
|
||||
{facetConfig.enabled && (
|
||||
<Link to="/admin/facets" className="admin-shell__link">{facetConfig.label || 'Facet'}</Link>
|
||||
)}
|
||||
{/* v0.8 — inventory entry point. Sits next to Products since
|
||||
it operates on the material-kind subset of the catalog. */}
|
||||
<Link to={adminInventoryListUrl} className="admin-shell__link">Inventory</Link>
|
||||
<Link to="/admin/featured" className="admin-shell__link">Featured</Link>
|
||||
<Link to="/admin/trash" className="admin-shell__link">Trash</Link>
|
||||
</div>
|
||||
<Link to="/" className="admin-shell__back">← Back to site</Link>
|
||||
</nav>
|
||||
<main className="admin-shell__main">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<p className="tm-editor__placeholder">
|
||||
Loading template metadata…
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-editor-canvas">
|
||||
<ApparelDesigner
|
||||
shirtColors={SHIRT_COLORS}
|
||||
shirtSizes={SHIRT_SIZES}
|
||||
basePrice={SHIRT_BASE_PRICE_USD}
|
||||
template={template}
|
||||
variant={variant}
|
||||
phoneModels={PHONE_MODELS}
|
||||
loadFonts={false}
|
||||
isAdmin
|
||||
onViewsChange={handleViewsChange}
|
||||
onUploadFile={handleUploadFile}
|
||||
// Marketing Gallery Entry (Round 6) — admin authoring. entryMode
|
||||
// 'direct' lands the admin in the editor (so the Stage + Marketing
|
||||
// tabs are reachable; the Marketing tab's Play button previews the
|
||||
// gallery); onEntryConfigChange persists Marketing-tab edits.
|
||||
entryConfig={effectiveEntryConfig}
|
||||
entryMode="direct"
|
||||
onEntryConfigChange={handleEntryConfigChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── /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 (
|
||||
<AdminShell>
|
||||
<TemplateListPanel onCreateClick={() => setCreateOpen(true)} />
|
||||
{createOpen && (
|
||||
<CreateTemplateModal
|
||||
defaultProductId={DEFAULT_PRODUCT_ID}
|
||||
defaultSizeId={DEFAULT_SIZE_ID}
|
||||
onConfirm={handleCreate}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// ── /admin/templates/:id — editor ──────────────────────────────
|
||||
function AdminTemplateEditorRoute() {
|
||||
const { id } = useParams();
|
||||
return (
|
||||
<AdminShell>
|
||||
<TemplateEditorPanel
|
||||
templateId={id}
|
||||
renderCanvas={(props) => <AdminEditorCanvas {...props} />}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// ── /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 (
|
||||
<AdminShell>
|
||||
<ProductListPanel onCreateClick={() => setCreateOpen(true)} />
|
||||
{createOpen && (
|
||||
<CreateProductModal
|
||||
onConfirm={handleCreate}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// ── /admin/products/:id — editor ───────────────────────────────
|
||||
function AdminProductEditorRoute() {
|
||||
const { id } = useParams();
|
||||
return (
|
||||
<AdminShell>
|
||||
<ProductEditorPanel productId={id} />
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<AdminShell>
|
||||
<InventoryListPanel />
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// v0.8 — /admin/inventory/:id — single-variant detail.
|
||||
function AdminInventoryDetailRoute() {
|
||||
const { id } = useParams();
|
||||
return (
|
||||
<AdminShell>
|
||||
<InventoryDetailPanel variantId={id} />
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<AdminShell>
|
||||
<FacetValueManager availableStickerCategories={DEFAULT_STICKER_CATEGORIES} />
|
||||
</AdminShell>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="shop-page">
|
||||
<LoginForm next={next} appName="Pawfectly Yours" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppRouter() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<TemplateStoreProvider
|
||||
customizeUrl={customizeUrl}
|
||||
adminTemplateUrl={adminTemplateUrl}
|
||||
adminTemplateListUrl={adminTemplateListUrl}
|
||||
adminProductListUrl={adminProductListUrl}
|
||||
adminProductUrl={adminProductUrl}
|
||||
adminInventoryListUrl={adminInventoryListUrl}
|
||||
adminInventoryUrl={adminInventoryUrl}
|
||||
shopTemplateUrl={shopTemplateUrl}
|
||||
Link={Link}
|
||||
useNavigate={useNavigate}
|
||||
isAdmin
|
||||
apiBase="/api"
|
||||
>
|
||||
{/* 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. */}
|
||||
<FacetPrompt apiBase="/api" />
|
||||
<Routes>
|
||||
<Route path="/" element={<ShopHome />} />
|
||||
<Route path="/shop" element={<ShopHome />} />
|
||||
<Route path="/shop/:productSlug" element={<ProductPage />} />
|
||||
<Route path="/cart" element={<CartStub />} />
|
||||
<Route path="/customize" element={<CustomizeRoute />} />
|
||||
|
||||
{/* v0.8 — auth + account */}
|
||||
<Route path="/auth/login" element={<LoginRoute />} />
|
||||
<Route path="/admin/login" element={<AdminLoginPage />} />
|
||||
<Route path="/account" element={<AccountShell />}>
|
||||
<Route index element={<AccountHomePage />} />
|
||||
<Route path="addresses" element={<AccountAddressesPage />} />
|
||||
{/* Facet personalization (Project doc §R10) — AccountFacetPage
|
||||
hides itself when no facet is configured. */}
|
||||
<Route path="facet" element={<AccountFacetPage />} />
|
||||
{/* /account/designs lands once goods-editor-module exports a
|
||||
MyDesignsPage component (Phase 2 / v08_GoodsEditor_v1).
|
||||
/account/orders lands with commerce-module. */}
|
||||
</Route>
|
||||
|
||||
{/* Admin — wrapped in RequireAdmin (client guard). Server-
|
||||
side, the API endpoints still need requireAdmin
|
||||
middleware once template-manager + commerce-module wire
|
||||
it in. */}
|
||||
<Route path={adminTemplateListUrl} element={<RequireAdmin><AdminTemplateListRoute /></RequireAdmin>} />
|
||||
<Route path="/admin/templates/:id" element={<RequireAdmin><AdminTemplateEditorRoute /></RequireAdmin>} />
|
||||
<Route path={adminProductListUrl} element={<RequireAdmin><AdminProductListRoute /></RequireAdmin>} />
|
||||
<Route path="/admin/products/:id" element={<RequireAdmin><AdminProductEditorRoute /></RequireAdmin>} />
|
||||
{/* v0.8 — inventory routes. */}
|
||||
<Route path={adminInventoryListUrl} element={<RequireAdmin><AdminInventoryListRoute /></RequireAdmin>} />
|
||||
<Route path="/admin/inventory/:id" element={<RequireAdmin><AdminInventoryDetailRoute /></RequireAdmin>} />
|
||||
{/* Facet personalization (Project doc §R10). */}
|
||||
<Route path="/admin/facets" element={<RequireAdmin><AdminFacetRoute /></RequireAdmin>} />
|
||||
<Route path="/admin/variants" element={<RequireAdmin><AdminShell><VariantMockupPanel /></AdminShell></RequireAdmin>} />
|
||||
<Route path="/admin/featured" element={<RequireAdmin><AdminShell><FeaturedOrderPanel /></AdminShell></RequireAdmin>} />
|
||||
<Route path="/admin/trash" element={<RequireAdmin><AdminShell><TemplateTrashPanel /></AdminShell></RequireAdmin>} />
|
||||
|
||||
{/* SPA fallback — unknown routes land on the shop home. */}
|
||||
<Route path="*" element={<ShopHome />} />
|
||||
</Routes>
|
||||
</TemplateStoreProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user