v0.6: template-manager owns the product catalog
The host no longer carries a runtime product catalog. products.js shrinks to a legacy single-product stub; the canonical catalog lives in template-manager's productsStore, seeded ONCE on first boot from the new seedProducts.js (after that the store under templates/ is authoritative and the seed is ignored — intentional, so restarts never rewrite an admin-managed catalog). phoneModels.js and templateCategories.js are the supporting constant sets, and docs/template-schema.md documents the v4 schema. templates/ is the admin-managed store itself (_products_index.json + products/<id>/product.json, JSON only) — committed so the configured catalog (sizes, pricing, options built up in the admin UI) survives a fresh checkout rather than regressing to the seed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
This document describes the template structure for creating custom t-shirt design templates.
|
||||
|
||||
> **Note (2026-07-06):** this doc predates the goods-editor extraction and the
|
||||
> v4 multi-view template schema. The authoritative element schema lives in
|
||||
> `goods-editor-module/CLAUDE.md` ("Element schema") and the accepted payload
|
||||
> shape in `template-manager/src/server/lib/validation.js`. The property
|
||||
> tables below are still accurate for the fields they list.
|
||||
|
||||
## Template Structure
|
||||
|
||||
```json
|
||||
@@ -42,6 +48,27 @@ This document describes the template structure for creating custom t-shirt desig
|
||||
| `fontFamily`| string| No | Google Font name (default: "DM Sans") |
|
||||
| `fill` | string | No | Hex color (default: "#0f172a") |
|
||||
| `rotation` | number | No | Rotation in degrees (default: 0) |
|
||||
| `arc` | number | No | Curve amount (default: 0 = flat). `> 0` arcs up (rainbow), `< 0` arcs down (smile). See "Curved text" below. |
|
||||
| `align` | string | No | `left` \| `center` \| `right` (default: `center`; multi-line) |
|
||||
| `lineHeight` | number | No | Line-box ratio of fontSize (default: 1.2) |
|
||||
| `letterSpacing` | number | No | Absolute tracking in canvas units (default: 0; suspended while curved) |
|
||||
|
||||
#### Curved text (`arc`)
|
||||
|
||||
- The editor's Curve slider writes values in **−100..100**; hand-authored
|
||||
templates should stay in that range.
|
||||
- The stored value up to **±200** is honored by the renderer and the print
|
||||
exporter (`ARC_MODEL_MAX` in the goods-editor module, 2026-07-06): the
|
||||
editor itself *derives* values above 100 when preserving an authored
|
||||
curve on long customer text whose font size auto-shrank. Don't hand-author
|
||||
above ±100 — the headroom exists for derived values.
|
||||
- What is visually preserved across customer edits is the curve's
|
||||
**density** (`arc · fontSize / width`), not the raw number: the editor
|
||||
rescales `arc` as text, font, or size changes so the perceived sweep the
|
||||
template author designed stays constant. Expect saved customer designs to
|
||||
carry arc values that differ from the authored one.
|
||||
- `letterSpacing` has no effect while `|arc| > 0.5` (suspension rule); the
|
||||
stored value survives and reapplies when the text goes flat.
|
||||
|
||||
### Image Element
|
||||
|
||||
|
||||
174
src/constants/phoneModels.js
Normal file
174
src/constants/phoneModels.js
Normal file
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Phone model catalog for the phone-case product type.
|
||||
*
|
||||
* Coverage: iPhone 13 through iPhone 17 (21 models).
|
||||
*
|
||||
* Schema per entry
|
||||
* ─────────────────
|
||||
* id: kebab-case stable identifier (used in template IDs,
|
||||
* URLs, REST payloads)
|
||||
* label: human-readable name shown in pickers
|
||||
* series: '13' | '14' | '15' | '16' | '17' (for grouping)
|
||||
* body: { heightMm, widthMm, thicknessMm } — external case
|
||||
* dimensions, source: Apple specs + tech press as of
|
||||
* May 2026.
|
||||
* canvas: { width, height } — pixel dimensions of the editor
|
||||
* canvas. Aspect ratio matches body height:width
|
||||
* because the phone case wraps the back of the phone.
|
||||
* Canvas size 600 (vs 300 for shirts) gives finer
|
||||
* resolution for the typically-smaller phone case
|
||||
* designs.
|
||||
* cameraRegion: placeholder rectangle (or rectangles) in canvas
|
||||
* coordinates marking the camera bump cutout — the
|
||||
* non-printable area. Approximate for v1; production
|
||||
* ranges should refine these against real die-line
|
||||
* specs from the print shop.
|
||||
* silhouette: SVG path of the case outline in canvas coords.
|
||||
* v1: rounded-rect approximation; production should
|
||||
* use the actual die-line.
|
||||
*
|
||||
* Why placeholder camera regions
|
||||
* ──────────────────────────────
|
||||
* The plan explicitly says variant catalog ships as placeholders. Camera
|
||||
* regions on real production cases need exact die-line measurements from
|
||||
* the print shop — those come during pre-launch QA, not now. The
|
||||
* placeholder values are visually plausible (right corner, right size)
|
||||
* so the editor's UX of "you can't put your design here" works end-to-end
|
||||
* for testing. Replace with real die-line coordinates before launch.
|
||||
*/
|
||||
|
||||
// Canvas height (longest dimension); width is derived from each model's
|
||||
// body aspect ratio. Square-ish canvases tend to give the editor's
|
||||
// transformer controls room without the controls cropping off-screen.
|
||||
const CANVAS_LONG_EDGE = 600;
|
||||
|
||||
function buildCanvas(heightMm, widthMm) {
|
||||
// Phone is taller than wide; canvas height matches the long edge.
|
||||
const aspect = widthMm / heightMm;
|
||||
return {
|
||||
width: Math.round(CANVAS_LONG_EDGE * aspect),
|
||||
height: CANVAS_LONG_EDGE,
|
||||
};
|
||||
}
|
||||
|
||||
// Camera-region pattern helpers. Each returns a rectangle (or array of
|
||||
// them for compound bumps) sized as a fraction of the canvas, top-left
|
||||
// origin. The actual phone case manufacturer crops circles inside these
|
||||
// rects; we use rects because they're the conservative non-printable
|
||||
// area that contains the cutouts.
|
||||
function squareCorner(canvas) {
|
||||
// iPhone 13-15 base: square bump in top-left corner.
|
||||
const w = canvas.width * 0.32;
|
||||
const h = canvas.width * 0.32; // square in canvas units
|
||||
return [{ x: canvas.width * 0.05, y: canvas.width * 0.05, width: w, height: h }];
|
||||
}
|
||||
function verticalStrip(canvas) {
|
||||
// iPhone 16 / 16e / 16-plus / 17 / 17-air: vertical pill, top-left.
|
||||
const w = canvas.width * 0.14;
|
||||
const h = canvas.width * 0.34;
|
||||
return [{ x: canvas.width * 0.06, y: canvas.width * 0.05, width: w, height: h }];
|
||||
}
|
||||
function triangularBump(canvas) {
|
||||
// iPhone 16-pro / 16-pro-max: triangular three-lens cluster top-left.
|
||||
// Use the conservative bounding rect.
|
||||
const w = canvas.width * 0.34;
|
||||
const h = canvas.width * 0.34;
|
||||
return [{ x: canvas.width * 0.04, y: canvas.width * 0.04, width: w, height: h }];
|
||||
}
|
||||
function plateauBar(canvas) {
|
||||
// iPhone 17-pro / 17-pro-max: horizontal "plateau" bar spanning most
|
||||
// of the back top edge.
|
||||
const w = canvas.width * 0.88;
|
||||
const h = canvas.width * 0.18;
|
||||
return [{ x: canvas.width * 0.06, y: canvas.width * 0.04, width: w, height: h }];
|
||||
}
|
||||
|
||||
// Helper: build a rounded-rect silhouette path in canvas coords.
|
||||
// (M…L…A…Z form; corner radius ≈ 8% of width which matches Apple's
|
||||
// hardware roundness reasonably for v1 visualization.)
|
||||
function silhouettePath(canvas) {
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
const r = Math.round(w * 0.08);
|
||||
return `M${r},0 L${w - r},0 A${r},${r} 0 0 1 ${w},${r} L${w},${h - r} A${r},${r} 0 0 1 ${w - r},${h} L${r},${h} A${r},${r} 0 0 1 0,${h - r} L0,${r} A${r},${r} 0 0 1 ${r},0 Z`;
|
||||
}
|
||||
|
||||
function buildModel({ id, label, series, heightMm, widthMm, thicknessMm, cameraPattern }) {
|
||||
const canvas = buildCanvas(heightMm, widthMm);
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
series,
|
||||
body: { heightMm, widthMm, thicknessMm },
|
||||
canvas,
|
||||
cameraRegion: cameraPattern(canvas),
|
||||
silhouettePath: silhouettePath(canvas),
|
||||
};
|
||||
}
|
||||
|
||||
// Dimensions from Apple official specs + tech-press round-ups (Apr–May 2026):
|
||||
// • iPhone 13 line: Apple spec sheets
|
||||
// • iPhone 14 line: Apple spec sheets
|
||||
// • iPhone 15 line: Apple spec sheets
|
||||
// • iPhone 16 line + 16e: Apple spec sheets, ecoATM/SimplyMac aggregates
|
||||
// • iPhone 17 line: TechCrunch lineup comparison + Apple specs
|
||||
|
||||
export const PHONE_MODELS = Object.fromEntries([
|
||||
// iPhone 13 (4 models) — square camera top-left
|
||||
buildModel({ id: 'iphone-13-mini', label: 'iPhone 13 mini', series: '13', heightMm: 131.5, widthMm: 64.2, thicknessMm: 7.65, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-13', label: 'iPhone 13', series: '13', heightMm: 146.7, widthMm: 71.5, thicknessMm: 7.65, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-13-pro', label: 'iPhone 13 Pro', series: '13', heightMm: 146.7, widthMm: 71.5, thicknessMm: 7.65, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-13-pro-max', label: 'iPhone 13 Pro Max', series: '13', heightMm: 160.8, widthMm: 78.1, thicknessMm: 7.65, cameraPattern: squareCorner }),
|
||||
|
||||
// iPhone 14 (4 models) — square camera top-left, slightly larger on Pros
|
||||
buildModel({ id: 'iphone-14', label: 'iPhone 14', series: '14', heightMm: 146.7, widthMm: 71.5, thicknessMm: 7.8, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-14-plus', label: 'iPhone 14 Plus', series: '14', heightMm: 160.8, widthMm: 78.1, thicknessMm: 7.8, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-14-pro', label: 'iPhone 14 Pro', series: '14', heightMm: 147.5, widthMm: 71.5, thicknessMm: 7.85, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-14-pro-max', label: 'iPhone 14 Pro Max', series: '14', heightMm: 160.7, widthMm: 77.6, thicknessMm: 7.85, cameraPattern: squareCorner }),
|
||||
|
||||
// iPhone 15 (4 models) — square camera continues
|
||||
buildModel({ id: 'iphone-15', label: 'iPhone 15', series: '15', heightMm: 147.6, widthMm: 71.6, thicknessMm: 7.8, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-15-plus', label: 'iPhone 15 Plus', series: '15', heightMm: 160.9, widthMm: 77.8, thicknessMm: 7.8, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-15-pro', label: 'iPhone 15 Pro', series: '15', heightMm: 146.6, widthMm: 70.6, thicknessMm: 8.25, cameraPattern: squareCorner }),
|
||||
buildModel({ id: 'iphone-15-pro-max', label: 'iPhone 15 Pro Max', series: '15', heightMm: 159.9, widthMm: 76.7, thicknessMm: 8.25, cameraPattern: squareCorner }),
|
||||
|
||||
// iPhone 16 (5 models) — vertical strip on base/16e/Plus, triangular on Pros
|
||||
buildModel({ id: 'iphone-16e', label: 'iPhone 16e', series: '16', heightMm: 138.4, widthMm: 67.0, thicknessMm: 7.8, cameraPattern: verticalStrip }),
|
||||
buildModel({ id: 'iphone-16', label: 'iPhone 16', series: '16', heightMm: 147.6, widthMm: 71.6, thicknessMm: 7.8, cameraPattern: verticalStrip }),
|
||||
buildModel({ id: 'iphone-16-plus', label: 'iPhone 16 Plus', series: '16', heightMm: 160.9, widthMm: 77.8, thicknessMm: 7.8, cameraPattern: verticalStrip }),
|
||||
buildModel({ id: 'iphone-16-pro', label: 'iPhone 16 Pro', series: '16', heightMm: 149.6, widthMm: 71.5, thicknessMm: 8.25, cameraPattern: triangularBump }),
|
||||
buildModel({ id: 'iphone-16-pro-max', label: 'iPhone 16 Pro Max', series: '16', heightMm: 163.0, widthMm: 77.6, thicknessMm: 8.25, cameraPattern: triangularBump }),
|
||||
|
||||
// iPhone 17 (4 models) — Pros get the new horizontal "plateau" bar
|
||||
buildModel({ id: 'iphone-17', label: 'iPhone 17', series: '17', heightMm: 149.6, widthMm: 71.5, thicknessMm: 7.95, cameraPattern: verticalStrip }),
|
||||
buildModel({ id: 'iphone-17-air', label: 'iPhone Air', series: '17', heightMm: 156.2, widthMm: 74.7, thicknessMm: 5.64, cameraPattern: verticalStrip }),
|
||||
buildModel({ id: 'iphone-17-pro', label: 'iPhone 17 Pro', series: '17', heightMm: 150.0, widthMm: 71.9, thicknessMm: 8.75, cameraPattern: plateauBar }),
|
||||
buildModel({ id: 'iphone-17-pro-max', label: 'iPhone 17 Pro Max', series: '17', heightMm: 163.4, widthMm: 78.0, thicknessMm: 8.75, cameraPattern: plateauBar }),
|
||||
].map((m) => [m.id, m]));
|
||||
|
||||
/**
|
||||
* Default phone model ID used when the user lands on the phone-case
|
||||
* customize route without specifying a model. Picks the most common
|
||||
* current-generation flagship as of catalog timestamp (May 2026).
|
||||
*/
|
||||
export const DEFAULT_PHONE_MODEL_ID = 'iphone-16';
|
||||
|
||||
/**
|
||||
* Group phone models by series for menus. The picker UI typically
|
||||
* shows series headers ("iPhone 17", "iPhone 16", ...) with the
|
||||
* models under each.
|
||||
*/
|
||||
export function groupBySeries(models = PHONE_MODELS) {
|
||||
const groups = {};
|
||||
for (const model of Object.values(models)) {
|
||||
if (!groups[model.series]) groups[model.series] = [];
|
||||
groups[model.series].push(model);
|
||||
}
|
||||
// Sort each series by canvas height descending (biggest first within
|
||||
// a generation feels right in pickers).
|
||||
for (const series of Object.keys(groups)) {
|
||||
groups[series].sort((a, b) => b.body.heightMm - a.body.heightMm);
|
||||
}
|
||||
// Return series in DESCENDING order (newest first).
|
||||
return Object.entries(groups).sort(([a], [b]) => b.localeCompare(a));
|
||||
}
|
||||
@@ -1,89 +1,69 @@
|
||||
/**
|
||||
* Product type configuration for the Pawfectly Yours editor.
|
||||
* Legacy single-product stub.
|
||||
*
|
||||
* The frontend rendering is parameterized by these objects so that the same
|
||||
* editor code can drive shirts, magnets, earrings, stickers, and so on. Only
|
||||
* code reads this — there is intentionally no UI for picking the active
|
||||
* product.
|
||||
* v0.6 — the host no longer owns a runtime product catalog. The
|
||||
* canonical catalog lives in template-manager (productsStore) and is
|
||||
* seeded once from src/constants/seedProducts.js.
|
||||
*
|
||||
* Coordinate systems
|
||||
* ──────────────────
|
||||
* Every product has TWO coordinate systems:
|
||||
* What's left in this file
|
||||
* ────────────────────────
|
||||
* The legacy single-product editor flow at `/` (apparel-designer's
|
||||
* App.jsx) calls `getActiveProduct()` and reads a small set of
|
||||
* shape-only fields off the result: `canvasSize`, `printZone`,
|
||||
* `productSvg`, `background`. We keep a minimal stub here so that
|
||||
* flow continues booting without re-architecting it for the v0.6
|
||||
* data shape.
|
||||
*
|
||||
* 1. **Design coordinates** (`canvasSize`) — the integer space in which
|
||||
* individual elements (images, text, stickers) live. Element x/y/width/
|
||||
* height are in these units. The server export pipeline scales from
|
||||
* `canvasSize` to the final 4500×4500 print resolution.
|
||||
*
|
||||
* 2. **Product SVG coordinates** (`productSvg.width / .height`) — the
|
||||
* viewBox of the product mockup that's drawn behind the design canvas.
|
||||
* The `printZone` field positions the design canvas inside the product
|
||||
* mockup, in these units.
|
||||
*
|
||||
* Keeping `canvasSize` at 300 (the historical value) means existing templates
|
||||
* and the server-side export math do not need to change. The visual fit
|
||||
* (canvas overlapping the print zone of the product) is achieved by CSS
|
||||
* scaling the rendered Konva Stage at runtime.
|
||||
* Migration intent
|
||||
* ────────────────
|
||||
* The legacy `/` flow is slated for retirement; the customer-facing
|
||||
* surface is `/customize?template=…` which reads the template's
|
||||
* embedded variant. When the legacy flow is removed, this file goes
|
||||
* with it.
|
||||
*/
|
||||
|
||||
export const PRODUCT_TYPES = {
|
||||
shirt: {
|
||||
id: 'shirt',
|
||||
label: 'T-Shirt',
|
||||
canvasSize: 300,
|
||||
// Where on the product mockup the design canvas should overlay, in
|
||||
// product SVG coordinates. Sized roughly to a real DTG print zone for
|
||||
// a youth-medium tee.
|
||||
//
|
||||
// Note: the product SVG's viewBox is offset (starts at 40, 50) to crop
|
||||
// empty margin around the shirt outline. printZone coords are in that
|
||||
// same viewBox space — i.e. (90, 130) is what the chest landed at
|
||||
// after the crop, not the path's raw (130, 180).
|
||||
printZone: { x: 90, y: 130, width: 220, height: 220 },
|
||||
productSvg: {
|
||||
// Component name (looked up by DesignCanvas) and the dimensions of its
|
||||
// viewBox. Must match the actual SVG.
|
||||
component: 'shirt',
|
||||
width: 400,
|
||||
height: 470,
|
||||
},
|
||||
background: {
|
||||
// 'color' = user can pick from SHIRT_COLORS
|
||||
// 'fixed' = use background.color as a constant fill
|
||||
mode: 'color',
|
||||
defaultColorId: 'white',
|
||||
},
|
||||
},
|
||||
|
||||
// Reference shapes for future products. None of these are wired into the
|
||||
// UI yet, but the DesignCanvas/ProductSVG plumbing is ready to handle them.
|
||||
//
|
||||
// magnet: {
|
||||
// id: 'magnet',
|
||||
// label: 'Magnet',
|
||||
// canvasSize: 300,
|
||||
// printZone: { x: 30, y: 30, width: 240, height: 240 },
|
||||
// productSvg: { component: 'magnet', width: 300, height: 300 },
|
||||
// background: { mode: 'fixed', color: '#f4f1ee' },
|
||||
// },
|
||||
// earring: { ... },
|
||||
// sticker: { ... },
|
||||
// Bare shirt shape with just enough fields for the legacy
|
||||
// getActiveProduct() consumers in App.jsx + the editor's
|
||||
// legacy single-product code paths.
|
||||
const LEGACY_SHIRT_STUB = {
|
||||
id: 'shirt',
|
||||
label: 'T-Shirt',
|
||||
canvasSize: 300,
|
||||
printZone: { x: 90, y: 130, width: 220, height: 220 },
|
||||
productSvg: { component: 'shirt', width: 400, height: 470 },
|
||||
background: { mode: 'color', defaultColorId: 'white' },
|
||||
// No variants — v0.6 catalog lives in template-manager.
|
||||
variants: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Which product the editor is currently configured for. Change this constant
|
||||
* to point at a different `PRODUCT_TYPES` entry. There's intentionally no UI
|
||||
* exposure — switching products is a code change.
|
||||
* Return the active product for the legacy single-product editor.
|
||||
* Always returns the shirt stub in v0.6.
|
||||
*/
|
||||
export function getActiveProduct() {
|
||||
return LEGACY_SHIRT_STUB;
|
||||
}
|
||||
|
||||
// ── Deprecated exports ────────────────────────────────────────
|
||||
//
|
||||
// These names are referenced in places that haven't been swept yet.
|
||||
// They resolve to the legacy stub or to undefined so any straggling
|
||||
// importer doesn't crash on module load. Will be removed in a
|
||||
// follow-up cleanup pass.
|
||||
|
||||
/** @deprecated Use seedProducts.DEFAULT_PRODUCT_ID. */
|
||||
export const DEFAULT_PRODUCT_TYPE_ID = 'shirt';
|
||||
|
||||
/** @deprecated Use seedProducts.DEFAULT_SIZE_ID. */
|
||||
export const DEFAULT_VARIANT_ID = 'shirt-default';
|
||||
|
||||
/** @deprecated Use seedProducts. */
|
||||
export const ACTIVE_PRODUCT_ID = 'shirt';
|
||||
|
||||
export function getActiveProduct() {
|
||||
const product = PRODUCT_TYPES[ACTIVE_PRODUCT_ID];
|
||||
if (!product) {
|
||||
throw new Error(
|
||||
`Unknown ACTIVE_PRODUCT_ID="${ACTIVE_PRODUCT_ID}". `
|
||||
+ `Valid ids: ${Object.keys(PRODUCT_TYPES).join(', ')}`
|
||||
);
|
||||
}
|
||||
return product;
|
||||
/** @deprecated Use seedProducts; map productId/sizeId via the products API. */
|
||||
export const PRODUCT_TYPES = { shirt: LEGACY_SHIRT_STUB };
|
||||
|
||||
/** @deprecated Use the resolved variant from a template GET response. */
|
||||
export function getVariant(_productTypeId, _variantId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
445
src/constants/seedProducts.js
Normal file
445
src/constants/seedProducts.js
Normal file
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* Seed product catalog — used by template-manager's mountTemplateStore
|
||||
* to populate `_products_index.json` on first boot only.
|
||||
*
|
||||
* v0.6 — replaces the pre-v0.6 PRODUCT_TYPES constant. The host no
|
||||
* longer owns the catalog at runtime; this file is read once at boot
|
||||
* and the data is then persisted in the storage dir.
|
||||
*
|
||||
* UUIDs are hardcoded so re-running the seed (after a wipe) produces
|
||||
* the same identifiers, and so any test fixtures / docs that
|
||||
* reference these IDs stay stable.
|
||||
*
|
||||
* Schema (per product)
|
||||
* ────────────────────
|
||||
* { id, slug, label, description, category, kind, options[], sizes[] }
|
||||
* kind: 'customizable' | 'fixed' | 'material' (v0.7 + v0.8)
|
||||
*
|
||||
* Schema (per option value)
|
||||
* ─────────────────────────
|
||||
* { id, slug, label, hex?, priceCents }
|
||||
* priceCents is v0.8 — non-negative integer, default 0; the unit
|
||||
* price the customer pays is `size.basePriceCents + sum(selected
|
||||
* values' priceCents)`.
|
||||
*
|
||||
* Schema (per size)
|
||||
* ─────────────────
|
||||
* { id, slug, label,
|
||||
* physical: { width, height, unit: 'in' },
|
||||
* canvas: { width, height } | null, // null for fixed + material
|
||||
* printZone: { x, y, width, height } | null, // null for fixed + material
|
||||
* silhouettePath, mockupFallbackColor, basePriceCents,
|
||||
* defaultParcel: { length_in, width_in, height_in, weight_oz } } // v0.8
|
||||
*
|
||||
* Canvas convention
|
||||
* ─────────────────
|
||||
* 100 canvas units per physical inch (per locked decision §6). Exports
|
||||
* at 300 DPI map cleanly via 3× resample on the export pipeline.
|
||||
*
|
||||
* Print zone convention
|
||||
* ─────────────────────
|
||||
* 10% inset from canvas edges by default. Each size carries an
|
||||
* explicit rectangle so the admin can tune per-size if needed.
|
||||
*/
|
||||
|
||||
// ── Pre-allocated UUIDs ────────────────────────────────────────
|
||||
//
|
||||
// Hardcoded so the seed is idempotent: wiping the storage dir and
|
||||
// re-seeding produces identical IDs, which means existing template
|
||||
// references (if any survived the wipe) still resolve.
|
||||
//
|
||||
// Naming: P_<slug> for products, S_<slug>_<size-slug> for sizes,
|
||||
// O_<slug>_<option-slug> for option groups, V_<slug>_<option-slug>_<value-slug>
|
||||
// for option values.
|
||||
const P_SHIRT = 'c3ac22c7-ef49-4997-a525-d4f0eb32da6f';
|
||||
const P_HOODIE = '6202b99a-a4a9-4f11-8b2d-32c17516ecfa';
|
||||
const P_MAGNET = 'f530dc06-ef10-46b4-9903-3927f85bd9a7';
|
||||
const P_MUG = '951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd';
|
||||
const P_POSTER = '95dca14e-28e4-46e8-93b1-b45e3b556530';
|
||||
const P_TOTE = '1d69616b-96a8-4280-9beb-c8926222c589';
|
||||
const P_PILLOW = 'c722b19b-d6c6-4639-b0eb-dec34731e50d';
|
||||
const P_NOTEBOOK = '1ed028ad-1998-4ace-813b-79023bd08c1d';
|
||||
const P_STICKER = '04507704-50a9-4de9-9b23-475d6b76bf0e';
|
||||
const P_HAT = 'cb43b749-05c0-4d1c-8d0b-dadd9b28574d';
|
||||
const P_PHONE_CASE = 'f5cbb673-8696-4dad-a63c-3a802ba53fd3';
|
||||
// v0.7 — first fixed-kind (ready-made) product. No customization;
|
||||
// customer picks size + color and adds to cart.
|
||||
const P_RABBIT_TEE = '8b1f4c2d-3e9a-4b76-9c8e-2a1d5f6e7b89';
|
||||
|
||||
const S_SHIRT_S = '3df58f51-70ce-4898-8091-2bb186b04193';
|
||||
const S_SHIRT_M = '86787c88-c4e0-4798-a34f-d1e173b2704b';
|
||||
const S_SHIRT_L = '8cf5377f-8c6b-4afe-80c3-9337e6303176';
|
||||
const S_SHIRT_XL = 'ecc7707a-1703-4c27-9da7-6b39709837ba';
|
||||
const S_SHIRT_XXL = '3667ec33-6123-44bd-a12e-5fdfef3c2874';
|
||||
|
||||
const S_HOODIE_S = 'f002d98e-6601-46cb-9ed0-ad6608a794ac';
|
||||
const S_HOODIE_M = '13849307-bed4-4654-9476-fb135561f05f';
|
||||
const S_HOODIE_L = 'b327d6c5-8f0b-4639-b8ec-8d3f4ec74d1a';
|
||||
const S_HOODIE_XL = 'af9ed947-e705-4205-87b0-6c529ce6b342';
|
||||
const S_HOODIE_XXL = '83156320-5f30-4cf8-b28c-3f69fcf7887e';
|
||||
|
||||
const S_MAGNET_3X3 = '6f88ed75-7ab5-49ae-8dc5-18568da63899';
|
||||
const S_MAGNET_4X4 = '3a9d60f5-821c-4f3f-8d41-62cfeaba208a';
|
||||
const S_MAGNET_ROUND = '17e3cc66-f897-4717-8b08-c5b0308668df';
|
||||
|
||||
const S_MUG_11OZ = 'ccaf7033-3e43-4171-9cde-fe6be676ea05';
|
||||
const S_MUG_15OZ = 'ae4ad837-6669-4491-af8e-445f90794779';
|
||||
|
||||
const S_POSTER_12X18 = '7d015072-f884-4e04-8a39-ce7c8e149059';
|
||||
const S_POSTER_18X24 = '9a5529fd-e54e-4868-acbb-5a62c44d1297';
|
||||
|
||||
const S_TOTE_14X14 = '61e5519d-1230-4397-a795-7303a30e0881';
|
||||
const S_PILLOW_18 = '22d04708-d964-4cdc-a682-fa9c02372a88';
|
||||
const S_NOTEBOOK_A5 = 'add070b4-1fa3-47bb-84a5-df8d99c2b821';
|
||||
const S_STICKER_3 = '6744bd80-e64b-4351-a0b8-9316dd138db5';
|
||||
const S_HAT_CAP = '778c4b47-a46b-4753-8fa2-be8af1523f4a';
|
||||
const S_PHONE_DEFAULT = '4be821c2-8ba5-4dd1-ad0b-1a188dd89796';
|
||||
|
||||
// Rabbit Tee (v0.7 fixed product) — same 5 apparel sizes as the
|
||||
// shirt, with the same shape but distinct UUIDs so they're separable
|
||||
// in the index + audit log.
|
||||
const S_RABBIT_S = 'a1b2c3d4-e5f6-4a78-8b9c-1d2e3f4a5b6c';
|
||||
const S_RABBIT_M = 'b2c3d4e5-f6a7-4b89-9c8d-2e3f4a5b6c7d';
|
||||
const S_RABBIT_L = 'c3d4e5f6-a7b8-4c9d-8a9e-3f4a5b6c7d8e';
|
||||
const S_RABBIT_XL = 'd4e5f6a7-b8c9-4d8e-9b8a-4a5b6c7d8e9f';
|
||||
const S_RABBIT_XXL = 'e5f6a7b8-c9d8-4e8f-8c9b-5b6c7d8e9f8a';
|
||||
|
||||
// Option group + value UUIDs (shirt + hoodie colors).
|
||||
const O_SHIRT_COLOR = '9d58a8b1-5976-4512-9095-883f16a4c1b8';
|
||||
const V_SHIRT_WHITE = '29092f97-2654-4150-89e5-7a323eb891e4';
|
||||
const V_SHIRT_BLACK = 'c25f746a-efc2-4213-9328-728ac6881bc6';
|
||||
const V_SHIRT_PINK = '8e077db8-bc48-45c0-a3e1-df10b6f1367f';
|
||||
|
||||
const O_HOODIE_COLOR = '49a9bd93-84c9-4e9c-a4e6-c1e742c6777a';
|
||||
const V_HOODIE_BLACK = '4e8d2854-8f0e-4c99-b854-8c0e07b59a49';
|
||||
const V_HOODIE_NAVY = '618c49fc-9fc6-486b-9d83-a2fff373092f';
|
||||
const V_HOODIE_GRAY = '0d1d8ec2-c7b9-40c9-9da8-9b5f1c6d1e3f';
|
||||
|
||||
// Rabbit tee (v0.7) color option.
|
||||
const O_RABBIT_COLOR = 'f6a7b8c9-d8e9-4f8a-9d8e-6c7d8e9f8a7b';
|
||||
const V_RABBIT_WHITE = 'a7b8c9d8-e9f8-4a9b-8e9f-7d8e9f8a7b6c';
|
||||
const V_RABBIT_BLACK = 'b8c9d8e9-f8a7-4b8c-9f8a-8e9f8a7b6c5d';
|
||||
const V_RABBIT_PINK = 'c9d8e9f8-a7b6-4c8d-8a7b-9f8a7b6c5d4e';
|
||||
|
||||
// ── Geometry helpers ──────────────────────────────────────────
|
||||
//
|
||||
// Same helpers the pre-v0.6 PRODUCT_TYPES used, lifted here so the
|
||||
// seed file is self-contained (no import from the legacy products.js,
|
||||
// which is being stripped down in this same PR).
|
||||
|
||||
function roundedRectPath(w, h, r) {
|
||||
return `M${r},0 L${w - r},0 A${r},${r} 0 0 1 ${w},${r} L${w},${h - r} A${r},${r} 0 0 1 ${w - r},${h} L${r},${h} A${r},${r} 0 0 1 0,${h - r} L0,${r} A${r},${r} 0 0 1 ${r},0 Z`;
|
||||
}
|
||||
|
||||
function circlePath(r) {
|
||||
return `M${r},0 A${r},${r} 0 1 0 ${r},${2 * r} A${r},${r} 0 1 0 ${r},0 Z`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a printZone rect inset by `pct` (0..1) from canvas edges.
|
||||
*/
|
||||
function insetPrintZone(canvasW, canvasH, pct = 0.1) {
|
||||
const insetW = Math.round(canvasW * pct);
|
||||
const insetH = Math.round(canvasH * pct);
|
||||
return {
|
||||
x: insetW,
|
||||
y: insetH,
|
||||
width: canvasW - 2 * insetW,
|
||||
height: canvasH - 2 * insetH,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a size record for a fixed (ready-made) product. No canvas,
|
||||
* no print zone, no silhouette — just physical dimensions + price.
|
||||
* Used by v0.7 fixed-kind products like the rabbit tee.
|
||||
*/
|
||||
function makeFixedSize({ id, slug, label, widthIn, heightIn, fallbackColor, basePriceCents = 0 }) {
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
label,
|
||||
physical: { width: widthIn, height: heightIn, unit: 'in' },
|
||||
canvas: null,
|
||||
printZone: null,
|
||||
silhouettePath: null,
|
||||
mockupFallbackColor: fallbackColor,
|
||||
basePriceCents,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a size record for a rectangular product with physical inches
|
||||
* x 100 canvas units. The helper centralizes the conversion so a
|
||||
* future canvas-units change (e.g. moving to 144 units/inch) is a
|
||||
* one-place edit.
|
||||
*/
|
||||
function makeRectSize({ id, slug, label, widthIn, heightIn, fallbackColor, basePriceCents = 0, printZonePct = 0.1, cornerPct = 0.04 }) {
|
||||
const cW = widthIn * 100;
|
||||
const cH = heightIn * 100;
|
||||
const r = Math.round(Math.min(cW, cH) * cornerPct);
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
label,
|
||||
physical: { width: widthIn, height: heightIn, unit: 'in' },
|
||||
canvas: { width: cW, height: cH },
|
||||
printZone: insetPrintZone(cW, cH, printZonePct),
|
||||
silhouettePath: roundedRectPath(cW, cH, r),
|
||||
mockupFallbackColor: fallbackColor,
|
||||
basePriceCents,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCircleSize({ id, slug, label, diameterIn, fallbackColor, basePriceCents = 0, printZonePct = 0.1 }) {
|
||||
const c = diameterIn * 100;
|
||||
const r = c / 2;
|
||||
const inset = Math.round(c * printZonePct);
|
||||
return {
|
||||
id,
|
||||
slug,
|
||||
label,
|
||||
physical: { width: diameterIn, height: diameterIn, unit: 'in' },
|
||||
canvas: { width: c, height: c },
|
||||
printZone: { x: inset, y: inset, width: c - 2 * inset, height: c - 2 * inset },
|
||||
silhouettePath: circlePath(r),
|
||||
mockupFallbackColor: fallbackColor,
|
||||
basePriceCents,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Seed catalog ──────────────────────────────────────────────
|
||||
|
||||
export const SEED_PRODUCTS = [
|
||||
// ── T-Shirt ────────────────────────────────────────────────
|
||||
{
|
||||
id: P_SHIRT,
|
||||
slug: 'shirt',
|
||||
label: 'T-Shirt',
|
||||
description: 'Soft cotton tee, printed on the front.',
|
||||
category: 'Apparel',
|
||||
options: [
|
||||
{
|
||||
id: O_SHIRT_COLOR,
|
||||
slug: 'color',
|
||||
label: 'Color',
|
||||
kind: 'color',
|
||||
values: [
|
||||
{ id: V_SHIRT_WHITE, slug: 'white', label: 'White', hex: '#ffffff' },
|
||||
{ id: V_SHIRT_BLACK, slug: 'black', label: 'Black', hex: '#1f2937' },
|
||||
{ id: V_SHIRT_PINK, slug: 'pink', label: 'Dusty Pink', hex: '#fbcfe8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_SHIRT_S, slug: 's', label: 'Small', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2200 }),
|
||||
makeRectSize({ id: S_SHIRT_M, slug: 'm', label: 'Medium', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2200 }),
|
||||
makeRectSize({ id: S_SHIRT_L, slug: 'l', label: 'Large', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2200 }),
|
||||
makeRectSize({ id: S_SHIRT_XL, slug: 'xl', label: 'X-Large', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2400 }),
|
||||
makeRectSize({ id: S_SHIRT_XXL, slug: 'xxl', label: 'XX-Large', widthIn: 12, heightIn: 16, fallbackColor: '#f4f4f5', basePriceCents: 2600 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Hoodie ─────────────────────────────────────────────────
|
||||
{
|
||||
id: P_HOODIE,
|
||||
slug: 'hoodie',
|
||||
label: 'Hoodie',
|
||||
description: 'Heavyweight pullover hoodie with front-and-back print options.',
|
||||
category: 'Apparel',
|
||||
options: [
|
||||
{
|
||||
id: O_HOODIE_COLOR,
|
||||
slug: 'color',
|
||||
label: 'Color',
|
||||
kind: 'color',
|
||||
values: [
|
||||
{ id: V_HOODIE_BLACK, slug: 'black', label: 'Black', hex: '#111827' },
|
||||
{ id: V_HOODIE_NAVY, slug: 'navy', label: 'Navy', hex: '#1e3a8a' },
|
||||
{ id: V_HOODIE_GRAY, slug: 'gray', label: 'Heather Gray', hex: '#9ca3af' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_HOODIE_S, slug: 's', label: 'Small', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 4800 }),
|
||||
makeRectSize({ id: S_HOODIE_M, slug: 'm', label: 'Medium', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 4800 }),
|
||||
makeRectSize({ id: S_HOODIE_L, slug: 'l', label: 'Large', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 4800 }),
|
||||
makeRectSize({ id: S_HOODIE_XL, slug: 'xl', label: 'X-Large', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 5200 }),
|
||||
makeRectSize({ id: S_HOODIE_XXL, slug: 'xxl', label: 'XX-Large', widthIn: 12, heightIn: 16, fallbackColor: '#475569', basePriceCents: 5400 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Magnet ─────────────────────────────────────────────────
|
||||
{
|
||||
id: P_MAGNET,
|
||||
slug: 'magnet',
|
||||
label: 'Magnet',
|
||||
description: 'Refrigerator-safe matte magnet, square or die-cut round.',
|
||||
category: 'Home',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_MAGNET_3X3, slug: '3x3-square', label: '3" × 3" Square', widthIn: 3, heightIn: 3, fallbackColor: '#fef9c3', basePriceCents: 700 }),
|
||||
makeRectSize({ id: S_MAGNET_4X4, slug: '4x4-square', label: '4" × 4" Square', widthIn: 4, heightIn: 4, fallbackColor: '#fef9c3', basePriceCents: 900 }),
|
||||
makeCircleSize({ id: S_MAGNET_ROUND, slug: '3in-round', label: '3" Round', diameterIn: 3, fallbackColor: '#fde68a', basePriceCents: 700 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Mug ────────────────────────────────────────────────────
|
||||
{
|
||||
id: P_MUG,
|
||||
slug: 'mug',
|
||||
label: 'Ceramic Mug',
|
||||
description: 'Dishwasher-safe ceramic mug. Print wraps the body.',
|
||||
category: 'Drinkware',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_MUG_11OZ, slug: '11oz', label: '11oz', widthIn: 8, heightIn: 3.25, fallbackColor: '#f8fafc', basePriceCents: 1500 }),
|
||||
makeRectSize({ id: S_MUG_15OZ, slug: '15oz', label: '15oz', widthIn: 8.5, heightIn: 4.25, fallbackColor: '#f1f5f9', basePriceCents: 1800 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Poster ─────────────────────────────────────────────────
|
||||
{
|
||||
id: P_POSTER,
|
||||
slug: 'poster',
|
||||
label: 'Poster',
|
||||
description: 'Premium matte poster, suitable for framing.',
|
||||
category: 'Wall Art',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_POSTER_12X18, slug: '12x18', label: '12" × 18"', widthIn: 12, heightIn: 18, fallbackColor: '#f3f4f6', basePriceCents: 1800 }),
|
||||
makeRectSize({ id: S_POSTER_18X24, slug: '18x24', label: '18" × 24"', widthIn: 18, heightIn: 24, fallbackColor: '#e5e7eb', basePriceCents: 2400 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Tote Bag ───────────────────────────────────────────────
|
||||
{
|
||||
id: P_TOTE,
|
||||
slug: 'tote',
|
||||
label: 'Tote Bag',
|
||||
description: 'Sturdy canvas tote with reinforced straps.',
|
||||
category: 'Accessories',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_TOTE_14X14, slug: '14x14', label: '14" × 14"', widthIn: 14, heightIn: 14, fallbackColor: '#fef3c7', basePriceCents: 1800 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Pillow ─────────────────────────────────────────────────
|
||||
{
|
||||
id: P_PILLOW,
|
||||
slug: 'pillow',
|
||||
label: 'Throw Pillow',
|
||||
description: 'Plush throw pillow with hidden zipper and insert.',
|
||||
category: 'Home',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_PILLOW_18, slug: '18x18', label: '18" × 18"', widthIn: 18, heightIn: 18, fallbackColor: '#dcfce7', basePriceCents: 2400, cornerPct: 0.09 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Notebook ───────────────────────────────────────────────
|
||||
{
|
||||
id: P_NOTEBOOK,
|
||||
slug: 'notebook',
|
||||
label: 'Notebook',
|
||||
description: 'A5 hardcover notebook, 120 lined pages.',
|
||||
category: 'Stationery',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_NOTEBOOK_A5, slug: 'a5', label: 'A5', widthIn: 5.5, heightIn: 8.5, fallbackColor: '#fafaf9', basePriceCents: 1500 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Sticker ────────────────────────────────────────────────
|
||||
{
|
||||
id: P_STICKER,
|
||||
slug: 'sticker',
|
||||
label: 'Sticker',
|
||||
description: 'Weatherproof vinyl sticker, die-cut to your design.',
|
||||
category: 'Stationery',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_STICKER_3, slug: '3in-die-cut', label: '3" Die-Cut', widthIn: 3, heightIn: 3, fallbackColor: '#fce7f3', basePriceCents: 400 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Hat ────────────────────────────────────────────────────
|
||||
{
|
||||
id: P_HAT,
|
||||
slug: 'hat',
|
||||
label: 'Hat',
|
||||
description: 'Adjustable baseball cap with embroidered front print.',
|
||||
category: 'Apparel',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_HAT_CAP, slug: 'cap', label: 'Baseball Cap', widthIn: 5, heightIn: 2, fallbackColor: '#fce7f3', basePriceCents: 2200, cornerPct: 0.15 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Phone Case ─────────────────────────────────────────────
|
||||
//
|
||||
// v0.6 — per-phone-model sizing is deferred. The single seed size
|
||||
// here uses a generic 2.7×5.4 inch canvas (~iPhone 13/14/15 wrap
|
||||
// area). When per-model sizing lands, this product will gain a
|
||||
// size per phone model (each with its own canvas).
|
||||
{
|
||||
id: P_PHONE_CASE,
|
||||
slug: 'phone-case',
|
||||
label: 'Phone Case',
|
||||
description: 'Slim flexible case with edge-to-edge print.',
|
||||
category: 'Accessories',
|
||||
options: [],
|
||||
sizes: [
|
||||
makeRectSize({ id: S_PHONE_DEFAULT, slug: 'generic', label: 'Generic Wrap', widthIn: 2.7, heightIn: 5.4, fallbackColor: '#e0e7ff', basePriceCents: 2200, cornerPct: 0.1 }),
|
||||
],
|
||||
},
|
||||
|
||||
// ── Rabbit T-Shirt (v0.7 fixed-kind example) ───────────────
|
||||
//
|
||||
// First ready-made product in the catalog. Pre-printed with a rabbit
|
||||
// illustration; customer picks size + color and adds to cart. No
|
||||
// editor mount, no templates can be pinned to it (server rejects).
|
||||
// images[] is empty at seed time — admin uploads gallery photos via
|
||||
// the product editor's new Photos section.
|
||||
{
|
||||
id: P_RABBIT_TEE,
|
||||
slug: 'rabbit-tee',
|
||||
label: 'Rabbit T-Shirt',
|
||||
description: 'Ready-made tee featuring our resident bunny mascot. Pre-printed, soft cotton, ships fast.',
|
||||
category: 'Apparel',
|
||||
kind: 'fixed',
|
||||
options: [
|
||||
{
|
||||
id: O_RABBIT_COLOR,
|
||||
slug: 'color',
|
||||
label: 'Color',
|
||||
kind: 'color',
|
||||
values: [
|
||||
{ id: V_RABBIT_WHITE, slug: 'white', label: 'White', hex: '#ffffff' },
|
||||
{ id: V_RABBIT_BLACK, slug: 'black', label: 'Black', hex: '#1f2937' },
|
||||
{ id: V_RABBIT_PINK, slug: 'pink', label: 'Dusty Pink', hex: '#fbcfe8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
sizes: [
|
||||
makeFixedSize({ id: S_RABBIT_S, slug: 's', label: 'Small', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2400 }),
|
||||
makeFixedSize({ id: S_RABBIT_M, slug: 'm', label: 'Medium', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2400 }),
|
||||
makeFixedSize({ id: S_RABBIT_L, slug: 'l', label: 'Large', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2400 }),
|
||||
makeFixedSize({ id: S_RABBIT_XL, slug: 'xl', label: 'X-Large', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2600 }),
|
||||
makeFixedSize({ id: S_RABBIT_XXL, slug: 'xxl', label: 'XX-Large', widthIn: 12, heightIn: 16, fallbackColor: '#fce7f3', basePriceCents: 2800 }),
|
||||
],
|
||||
images: [],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Convenience exports ────────────────────────────────────────
|
||||
//
|
||||
// Hosts using the legacy default-product flow at `/` still expect a
|
||||
// pair of `defaultProductId` + `defaultSizeId` they can hand to the
|
||||
// editor. Pre-v0.6 these were string slugs; v0.6 they're UUIDs. The
|
||||
// legacy App.jsx in apparel-designer reads these via the host
|
||||
// constants module (src/constants/products.js) which now re-exports
|
||||
// them from here.
|
||||
export const DEFAULT_PRODUCT_ID = P_SHIRT;
|
||||
export const DEFAULT_SIZE_ID = S_SHIRT_M;
|
||||
28
src/constants/templateCategories.js
Normal file
28
src/constants/templateCategories.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Template category catalog. Lifted from the legacy
|
||||
* goods-editor TEMPLATE_CATEGORIES constant — the 8 categories that
|
||||
* the original 8 templates used. Hosts can add categories ad-hoc
|
||||
* (templates store category as a free-text field), but having a
|
||||
* curated list makes the picker UI cleaner.
|
||||
*
|
||||
* The plan defers the more sophisticated "categories per product
|
||||
* type" structure to a future iteration; v1 is a flat list.
|
||||
*/
|
||||
|
||||
export const TEMPLATE_CATEGORIES = [
|
||||
'All',
|
||||
'Sports',
|
||||
'Music',
|
||||
'Quotes',
|
||||
'Animals',
|
||||
'Abstract',
|
||||
'Vintage',
|
||||
'Nature',
|
||||
'Tech',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default category for newly-created templates when admin hasn't
|
||||
* picked one explicitly.
|
||||
*/
|
||||
export const DEFAULT_CATEGORY = null;
|
||||
104
templates/_products_index.json
Normal file
104
templates/_products_index.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"products": {
|
||||
"c3ac22c7-ef49-4997-a525-d4f0eb32da6f": {
|
||||
"id": "c3ac22c7-ef49-4997-a525-d4f0eb32da6f",
|
||||
"slug": "shirt",
|
||||
"label": "T-Shirt",
|
||||
"category": "Apparel",
|
||||
"sizeCount": 5,
|
||||
"createdAt": "2026-05-25T12:31:21.547Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.547Z"
|
||||
},
|
||||
"6202b99a-a4a9-4f11-8b2d-32c17516ecfa": {
|
||||
"id": "6202b99a-a4a9-4f11-8b2d-32c17516ecfa",
|
||||
"slug": "hoodie",
|
||||
"label": "Hoodie",
|
||||
"category": "Apparel",
|
||||
"sizeCount": 5,
|
||||
"createdAt": "2026-05-25T12:31:21.552Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.552Z"
|
||||
},
|
||||
"f530dc06-ef10-46b4-9903-3927f85bd9a7": {
|
||||
"id": "f530dc06-ef10-46b4-9903-3927f85bd9a7",
|
||||
"slug": "magnet",
|
||||
"label": "Magnet",
|
||||
"category": "Home",
|
||||
"sizeCount": 3,
|
||||
"createdAt": "2026-05-25T12:31:21.554Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.554Z"
|
||||
},
|
||||
"951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd": {
|
||||
"id": "951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd",
|
||||
"slug": "mug",
|
||||
"label": "Ceramic Mug",
|
||||
"category": "Drinkware",
|
||||
"sizeCount": 2,
|
||||
"createdAt": "2026-05-25T12:31:21.555Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.555Z"
|
||||
},
|
||||
"95dca14e-28e4-46e8-93b1-b45e3b556530": {
|
||||
"id": "95dca14e-28e4-46e8-93b1-b45e3b556530",
|
||||
"slug": "poster",
|
||||
"label": "Poster",
|
||||
"category": "Wall Art",
|
||||
"sizeCount": 2,
|
||||
"createdAt": "2026-05-25T12:31:21.556Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.556Z"
|
||||
},
|
||||
"1d69616b-96a8-4280-9beb-c8926222c589": {
|
||||
"id": "1d69616b-96a8-4280-9beb-c8926222c589",
|
||||
"slug": "tote",
|
||||
"label": "Tote Bag",
|
||||
"category": "Accessories",
|
||||
"sizeCount": 1,
|
||||
"createdAt": "2026-05-25T12:31:21.557Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.557Z"
|
||||
},
|
||||
"c722b19b-d6c6-4639-b0eb-dec34731e50d": {
|
||||
"id": "c722b19b-d6c6-4639-b0eb-dec34731e50d",
|
||||
"slug": "pillow",
|
||||
"label": "Throw Pillow",
|
||||
"category": "Home",
|
||||
"sizeCount": 1,
|
||||
"createdAt": "2026-05-25T12:31:21.558Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.558Z"
|
||||
},
|
||||
"1ed028ad-1998-4ace-813b-79023bd08c1d": {
|
||||
"id": "1ed028ad-1998-4ace-813b-79023bd08c1d",
|
||||
"slug": "notebook",
|
||||
"label": "Notebook",
|
||||
"category": "Stationery",
|
||||
"sizeCount": 1,
|
||||
"createdAt": "2026-05-25T12:31:21.559Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.559Z"
|
||||
},
|
||||
"04507704-50a9-4de9-9b23-475d6b76bf0e": {
|
||||
"id": "04507704-50a9-4de9-9b23-475d6b76bf0e",
|
||||
"slug": "sticker",
|
||||
"label": "Sticker",
|
||||
"category": "Stationery",
|
||||
"sizeCount": 1,
|
||||
"createdAt": "2026-05-25T12:31:21.560Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.560Z"
|
||||
},
|
||||
"cb43b749-05c0-4d1c-8d0b-dadd9b28574d": {
|
||||
"id": "cb43b749-05c0-4d1c-8d0b-dadd9b28574d",
|
||||
"slug": "hat",
|
||||
"label": "Hat",
|
||||
"category": "Apparel",
|
||||
"sizeCount": 1,
|
||||
"createdAt": "2026-05-25T12:31:21.562Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.562Z"
|
||||
},
|
||||
"f5cbb673-8696-4dad-a63c-3a802ba53fd3": {
|
||||
"id": "f5cbb673-8696-4dad-a63c-3a802ba53fd3",
|
||||
"slug": "phone-case",
|
||||
"label": "Phone Case",
|
||||
"category": "Accessories",
|
||||
"sizeCount": 1,
|
||||
"createdAt": "2026-05-25T12:31:21.563Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.563Z"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "04507704-50a9-4de9-9b23-475d6b76bf0e",
|
||||
"slug": "sticker",
|
||||
"label": "Sticker",
|
||||
"description": "Weatherproof vinyl sticker, die-cut to your design.",
|
||||
"category": "Stationery",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "6744bd80-e64b-4351-a0b8-9316dd138db5",
|
||||
"slug": "3in-die-cut",
|
||||
"label": "3\" Die-Cut",
|
||||
"physical": {
|
||||
"width": 3,
|
||||
"height": 3,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 300,
|
||||
"height": 300
|
||||
},
|
||||
"printZone": {
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
"width": 240,
|
||||
"height": 240
|
||||
},
|
||||
"silhouettePath": "M12,0 L288,0 A12,12 0 0 1 300,12 L300,288 A12,12 0 0 1 288,300 L12,300 A12,12 0 0 1 0,288 L0,12 A12,12 0 0 1 12,0 Z",
|
||||
"mockupFallbackColor": "#fce7f3",
|
||||
"basePriceCents": 400
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.560Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.560Z"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "1d69616b-96a8-4280-9beb-c8926222c589",
|
||||
"slug": "tote",
|
||||
"label": "Tote Bag",
|
||||
"description": "Sturdy canvas tote with reinforced straps.",
|
||||
"category": "Accessories",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "61e5519d-1230-4397-a795-7303a30e0881",
|
||||
"slug": "14x14",
|
||||
"label": "14\" × 14\"",
|
||||
"physical": {
|
||||
"width": 14,
|
||||
"height": 14,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1400,
|
||||
"height": 1400
|
||||
},
|
||||
"printZone": {
|
||||
"x": 140,
|
||||
"y": 140,
|
||||
"width": 1120,
|
||||
"height": 1120
|
||||
},
|
||||
"silhouettePath": "M56,0 L1344,0 A56,56 0 0 1 1400,56 L1400,1344 A56,56 0 0 1 1344,1400 L56,1400 A56,56 0 0 1 0,1344 L0,56 A56,56 0 0 1 56,0 Z",
|
||||
"mockupFallbackColor": "#fef3c7",
|
||||
"basePriceCents": 1800
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.557Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.557Z"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "1ed028ad-1998-4ace-813b-79023bd08c1d",
|
||||
"slug": "notebook",
|
||||
"label": "Notebook",
|
||||
"description": "A5 hardcover notebook, 120 lined pages.",
|
||||
"category": "Stationery",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "add070b4-1fa3-47bb-84a5-df8d99c2b821",
|
||||
"slug": "a5",
|
||||
"label": "A5",
|
||||
"physical": {
|
||||
"width": 5.5,
|
||||
"height": 8.5,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 550,
|
||||
"height": 850
|
||||
},
|
||||
"printZone": {
|
||||
"x": 55,
|
||||
"y": 85,
|
||||
"width": 440,
|
||||
"height": 680
|
||||
},
|
||||
"silhouettePath": "M22,0 L528,0 A22,22 0 0 1 550,22 L550,828 A22,22 0 0 1 528,850 L22,850 A22,22 0 0 1 0,828 L0,22 A22,22 0 0 1 22,0 Z",
|
||||
"mockupFallbackColor": "#fafaf9",
|
||||
"basePriceCents": 1500
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.559Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.559Z"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"id": "6202b99a-a4a9-4f11-8b2d-32c17516ecfa",
|
||||
"slug": "hoodie",
|
||||
"label": "Hoodie",
|
||||
"description": "Heavyweight pullover hoodie with front-and-back print options.",
|
||||
"category": "Apparel",
|
||||
"options": [
|
||||
{
|
||||
"id": "49a9bd93-84c9-4e9c-a4e6-c1e742c6777a",
|
||||
"slug": "color",
|
||||
"label": "Color",
|
||||
"kind": "color",
|
||||
"values": [
|
||||
{
|
||||
"id": "4e8d2854-8f0e-4c99-b854-8c0e07b59a49",
|
||||
"slug": "black",
|
||||
"label": "Black",
|
||||
"hex": "#111827"
|
||||
},
|
||||
{
|
||||
"id": "618c49fc-9fc6-486b-9d83-a2fff373092f",
|
||||
"slug": "navy",
|
||||
"label": "Navy",
|
||||
"hex": "#1e3a8a"
|
||||
},
|
||||
{
|
||||
"id": "0d1d8ec2-c7b9-40c9-9da8-9b5f1c6d1e3f",
|
||||
"slug": "gray",
|
||||
"label": "Heather Gray",
|
||||
"hex": "#9ca3af"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "f002d98e-6601-46cb-9ed0-ad6608a794ac",
|
||||
"slug": "s",
|
||||
"label": "Small",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#475569",
|
||||
"basePriceCents": 4800
|
||||
},
|
||||
{
|
||||
"id": "13849307-bed4-4654-9476-fb135561f05f",
|
||||
"slug": "m",
|
||||
"label": "Medium",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#475569",
|
||||
"basePriceCents": 4800
|
||||
},
|
||||
{
|
||||
"id": "b327d6c5-8f0b-4639-b8ec-8d3f4ec74d1a",
|
||||
"slug": "l",
|
||||
"label": "Large",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#475569",
|
||||
"basePriceCents": 4800
|
||||
},
|
||||
{
|
||||
"id": "af9ed947-e705-4205-87b0-6c529ce6b342",
|
||||
"slug": "xl",
|
||||
"label": "X-Large",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#475569",
|
||||
"basePriceCents": 5200
|
||||
},
|
||||
{
|
||||
"id": "83156320-5f30-4cf8-b28c-3f69fcf7887e",
|
||||
"slug": "xxl",
|
||||
"label": "XX-Large",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#475569",
|
||||
"basePriceCents": 5400
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.552Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.552Z"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"id": "951cc0aa-6b28-460a-ad31-8ac8fdcc5ebd",
|
||||
"slug": "mug",
|
||||
"label": "Ceramic Mug",
|
||||
"description": "Dishwasher-safe ceramic mug. Print wraps the body.",
|
||||
"category": "Drinkware",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "ccaf7033-3e43-4171-9cde-fe6be676ea05",
|
||||
"slug": "11oz",
|
||||
"label": "11oz",
|
||||
"physical": {
|
||||
"width": 8,
|
||||
"height": 3.25,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 800,
|
||||
"height": 325
|
||||
},
|
||||
"printZone": {
|
||||
"x": 80,
|
||||
"y": 33,
|
||||
"width": 640,
|
||||
"height": 259
|
||||
},
|
||||
"silhouettePath": "M13,0 L787,0 A13,13 0 0 1 800,13 L800,312 A13,13 0 0 1 787,325 L13,325 A13,13 0 0 1 0,312 L0,13 A13,13 0 0 1 13,0 Z",
|
||||
"mockupFallbackColor": "#f8fafc",
|
||||
"basePriceCents": 1500
|
||||
},
|
||||
{
|
||||
"id": "ae4ad837-6669-4491-af8e-445f90794779",
|
||||
"slug": "15oz",
|
||||
"label": "15oz",
|
||||
"physical": {
|
||||
"width": 8.5,
|
||||
"height": 4.25,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 850,
|
||||
"height": 425
|
||||
},
|
||||
"printZone": {
|
||||
"x": 85,
|
||||
"y": 43,
|
||||
"width": 680,
|
||||
"height": 339
|
||||
},
|
||||
"silhouettePath": "M17,0 L833,0 A17,17 0 0 1 850,17 L850,408 A17,17 0 0 1 833,425 L17,425 A17,17 0 0 1 0,408 L0,17 A17,17 0 0 1 17,0 Z",
|
||||
"mockupFallbackColor": "#f1f5f9",
|
||||
"basePriceCents": 1800
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.555Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.555Z"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"id": "95dca14e-28e4-46e8-93b1-b45e3b556530",
|
||||
"slug": "poster",
|
||||
"label": "Poster",
|
||||
"description": "Premium matte poster, suitable for framing.",
|
||||
"category": "Wall Art",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "7d015072-f884-4e04-8a39-ce7c8e149059",
|
||||
"slug": "12x18",
|
||||
"label": "12\" × 18\"",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 18,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1800
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 180,
|
||||
"width": 960,
|
||||
"height": 1440
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1752 A48,48 0 0 1 1152,1800 L48,1800 A48,48 0 0 1 0,1752 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#f3f4f6",
|
||||
"basePriceCents": 1800
|
||||
},
|
||||
{
|
||||
"id": "9a5529fd-e54e-4868-acbb-5a62c44d1297",
|
||||
"slug": "18x24",
|
||||
"label": "18\" × 24\"",
|
||||
"physical": {
|
||||
"width": 18,
|
||||
"height": 24,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1800,
|
||||
"height": 2400
|
||||
},
|
||||
"printZone": {
|
||||
"x": 180,
|
||||
"y": 240,
|
||||
"width": 1440,
|
||||
"height": 1920
|
||||
},
|
||||
"silhouettePath": "M72,0 L1728,0 A72,72 0 0 1 1800,72 L1800,2328 A72,72 0 0 1 1728,2400 L72,2400 A72,72 0 0 1 0,2328 L0,72 A72,72 0 0 1 72,0 Z",
|
||||
"mockupFallbackColor": "#e5e7eb",
|
||||
"basePriceCents": 2400
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.556Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.556Z"
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"id": "c3ac22c7-ef49-4997-a525-d4f0eb32da6f",
|
||||
"slug": "shirt",
|
||||
"label": "T-Shirt",
|
||||
"description": "Soft cotton tee, printed on the front.",
|
||||
"category": "Apparel",
|
||||
"options": [
|
||||
{
|
||||
"id": "9d58a8b1-5976-4512-9095-883f16a4c1b8",
|
||||
"slug": "color",
|
||||
"label": "Color",
|
||||
"kind": "color",
|
||||
"values": [
|
||||
{
|
||||
"id": "29092f97-2654-4150-89e5-7a323eb891e4",
|
||||
"slug": "white",
|
||||
"label": "White",
|
||||
"hex": "#ffffff"
|
||||
},
|
||||
{
|
||||
"id": "c25f746a-efc2-4213-9328-728ac6881bc6",
|
||||
"slug": "black",
|
||||
"label": "Black",
|
||||
"hex": "#1f2937"
|
||||
},
|
||||
{
|
||||
"id": "8e077db8-bc48-45c0-a3e1-df10b6f1367f",
|
||||
"slug": "pink",
|
||||
"label": "Dusty Pink",
|
||||
"hex": "#fbcfe8"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "3df58f51-70ce-4898-8091-2bb186b04193",
|
||||
"slug": "s",
|
||||
"label": "Small",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#f4f4f5",
|
||||
"basePriceCents": 2200
|
||||
},
|
||||
{
|
||||
"id": "86787c88-c4e0-4798-a34f-d1e173b2704b",
|
||||
"slug": "m",
|
||||
"label": "Medium",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#f4f4f5",
|
||||
"basePriceCents": 2200
|
||||
},
|
||||
{
|
||||
"id": "8cf5377f-8c6b-4afe-80c3-9337e6303176",
|
||||
"slug": "l",
|
||||
"label": "Large",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#f4f4f5",
|
||||
"basePriceCents": 2200
|
||||
},
|
||||
{
|
||||
"id": "ecc7707a-1703-4c27-9da7-6b39709837ba",
|
||||
"slug": "xl",
|
||||
"label": "X-Large",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#f4f4f5",
|
||||
"basePriceCents": 2400
|
||||
},
|
||||
{
|
||||
"id": "3667ec33-6123-44bd-a12e-5fdfef3c2874",
|
||||
"slug": "xxl",
|
||||
"label": "XX-Large",
|
||||
"physical": {
|
||||
"width": 12,
|
||||
"height": 16,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1200,
|
||||
"height": 1600
|
||||
},
|
||||
"printZone": {
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"width": 960,
|
||||
"height": 1280
|
||||
},
|
||||
"silhouettePath": "M48,0 L1152,0 A48,48 0 0 1 1200,48 L1200,1552 A48,48 0 0 1 1152,1600 L48,1600 A48,48 0 0 1 0,1552 L0,48 A48,48 0 0 1 48,0 Z",
|
||||
"mockupFallbackColor": "#f4f4f5",
|
||||
"basePriceCents": 2600
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.547Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.547Z"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "c722b19b-d6c6-4639-b0eb-dec34731e50d",
|
||||
"slug": "pillow",
|
||||
"label": "Throw Pillow",
|
||||
"description": "Plush throw pillow with hidden zipper and insert.",
|
||||
"category": "Home",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "22d04708-d964-4cdc-a682-fa9c02372a88",
|
||||
"slug": "18x18",
|
||||
"label": "18\" × 18\"",
|
||||
"physical": {
|
||||
"width": 18,
|
||||
"height": 18,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 1800,
|
||||
"height": 1800
|
||||
},
|
||||
"printZone": {
|
||||
"x": 180,
|
||||
"y": 180,
|
||||
"width": 1440,
|
||||
"height": 1440
|
||||
},
|
||||
"silhouettePath": "M162,0 L1638,0 A162,162 0 0 1 1800,162 L1800,1638 A162,162 0 0 1 1638,1800 L162,1800 A162,162 0 0 1 0,1638 L0,162 A162,162 0 0 1 162,0 Z",
|
||||
"mockupFallbackColor": "#dcfce7",
|
||||
"basePriceCents": 2400
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.558Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.558Z"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "cb43b749-05c0-4d1c-8d0b-dadd9b28574d",
|
||||
"slug": "hat",
|
||||
"label": "Hat",
|
||||
"description": "Adjustable baseball cap with embroidered front print.",
|
||||
"category": "Apparel",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "778c4b47-a46b-4753-8fa2-be8af1523f4a",
|
||||
"slug": "cap",
|
||||
"label": "Baseball Cap",
|
||||
"physical": {
|
||||
"width": 5,
|
||||
"height": 2,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 500,
|
||||
"height": 200
|
||||
},
|
||||
"printZone": {
|
||||
"x": 50,
|
||||
"y": 20,
|
||||
"width": 400,
|
||||
"height": 160
|
||||
},
|
||||
"silhouettePath": "M30,0 L470,0 A30,30 0 0 1 500,30 L500,170 A30,30 0 0 1 470,200 L30,200 A30,30 0 0 1 0,170 L0,30 A30,30 0 0 1 30,0 Z",
|
||||
"mockupFallbackColor": "#fce7f3",
|
||||
"basePriceCents": 2200
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.562Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.562Z"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "f530dc06-ef10-46b4-9903-3927f85bd9a7",
|
||||
"slug": "magnet",
|
||||
"label": "Magnet",
|
||||
"description": "Refrigerator-safe matte magnet, square or die-cut round.",
|
||||
"category": "Home",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "6f88ed75-7ab5-49ae-8dc5-18568da63899",
|
||||
"slug": "3x3-square",
|
||||
"label": "3\" × 3\" Square",
|
||||
"physical": {
|
||||
"width": 3,
|
||||
"height": 3,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 300,
|
||||
"height": 300
|
||||
},
|
||||
"printZone": {
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
"width": 240,
|
||||
"height": 240
|
||||
},
|
||||
"silhouettePath": "M12,0 L288,0 A12,12 0 0 1 300,12 L300,288 A12,12 0 0 1 288,300 L12,300 A12,12 0 0 1 0,288 L0,12 A12,12 0 0 1 12,0 Z",
|
||||
"mockupFallbackColor": "#fef9c3",
|
||||
"basePriceCents": 700
|
||||
},
|
||||
{
|
||||
"id": "3a9d60f5-821c-4f3f-8d41-62cfeaba208a",
|
||||
"slug": "4x4-square",
|
||||
"label": "4\" × 4\" Square",
|
||||
"physical": {
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 400,
|
||||
"height": 400
|
||||
},
|
||||
"printZone": {
|
||||
"x": 40,
|
||||
"y": 40,
|
||||
"width": 320,
|
||||
"height": 320
|
||||
},
|
||||
"silhouettePath": "M16,0 L384,0 A16,16 0 0 1 400,16 L400,384 A16,16 0 0 1 384,400 L16,400 A16,16 0 0 1 0,384 L0,16 A16,16 0 0 1 16,0 Z",
|
||||
"mockupFallbackColor": "#fef9c3",
|
||||
"basePriceCents": 900
|
||||
},
|
||||
{
|
||||
"id": "17e3cc66-f897-4717-8b08-c5b0308668df",
|
||||
"slug": "3in-round",
|
||||
"label": "3\" Round",
|
||||
"physical": {
|
||||
"width": 3,
|
||||
"height": 3,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 300,
|
||||
"height": 300
|
||||
},
|
||||
"printZone": {
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
"width": 240,
|
||||
"height": 240
|
||||
},
|
||||
"silhouettePath": "M150,0 A150,150 0 1 0 150,300 A150,150 0 1 0 150,0 Z",
|
||||
"mockupFallbackColor": "#fde68a",
|
||||
"basePriceCents": 700
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.554Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.554Z"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "f5cbb673-8696-4dad-a63c-3a802ba53fd3",
|
||||
"slug": "phone-case",
|
||||
"label": "Phone Case",
|
||||
"description": "Slim flexible case with edge-to-edge print.",
|
||||
"category": "Accessories",
|
||||
"options": [],
|
||||
"sizes": [
|
||||
{
|
||||
"id": "4be821c2-8ba5-4dd1-ad0b-1a188dd89796",
|
||||
"slug": "generic",
|
||||
"label": "Generic Wrap",
|
||||
"physical": {
|
||||
"width": 2.7,
|
||||
"height": 5.4,
|
||||
"unit": "in"
|
||||
},
|
||||
"canvas": {
|
||||
"width": 270,
|
||||
"height": 540
|
||||
},
|
||||
"printZone": {
|
||||
"x": 27,
|
||||
"y": 54,
|
||||
"width": 216,
|
||||
"height": 432
|
||||
},
|
||||
"silhouettePath": "M27,0 L243,0 A27,27 0 0 1 270,27 L270,513 A27,27 0 0 1 243,540 L27,540 A27,27 0 0 1 0,513 L0,27 A27,27 0 0 1 27,0 Z",
|
||||
"mockupFallbackColor": "#e0e7ff",
|
||||
"basePriceCents": 2200
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-25T12:31:21.563Z",
|
||||
"updatedAt": "2026-05-25T12:31:21.563Z"
|
||||
}
|
||||
Reference in New Issue
Block a user