"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useField } from "@payloadcms/ui"; // ─── Types ────────────────────────────────────────────────────────── type MedalShape = "circle" | "star" | "cross" | "shield"; type MetalFinish = "gold" | "silver" | "bronze" | "copper" | "darkSteel"; type Stripe = { color: string; width: number; }; type MedalDesign = { ribbonStripes: Stripe[]; shape: MedalShape; finish: MetalFinish; engraving: string; }; // ─── Constants ────────────────────────────────────────────────────── const DEFAULT_MEDAL: MedalDesign = { ribbonStripes: [ { color: "#1e3a5f", width: 30 }, { color: "#ca8a04", width: 5 }, { color: "#b91c1c", width: 30 }, { color: "#ca8a04", width: 5 }, { color: "#1e3a5f", width: 30 }, ], shape: "circle", finish: "gold", engraving: "", }; const MILITARY_COLORS: { name: string; hex: string }[] = [ { name: "Scarlet", hex: "#b91c1c" }, { name: "Crimson", hex: "#991b1b" }, { name: "Dark Blue", hex: "#1e3a5f" }, { name: "Navy", hex: "#1e293b" }, { name: "Air Force Blue", hex: "#5b8fa8" }, { name: "Gold", hex: "#ca8a04" }, { name: "Yellow", hex: "#eab308" }, { name: "Forest Green", hex: "#166534" }, { name: "White", hex: "#f5f5f4" }, { name: "Silver / Gray", hex: "#9ca3af" }, { name: "Black", hex: "#18181b" }, { name: "Maroon", hex: "#581c87" }, { name: "Purple", hex: "#7e22ce" }, { name: "Brown", hex: "#78350f" }, { name: "Tan / Khaki", hex: "#d4a056" }, ]; const SHAPE_INFO: Record = { circle: { label: "Circle" }, star: { label: "Star" }, cross: { label: "Cross" }, shield: { label: "Shield" }, }; const FINISH_INFO: Record = { gold: { label: "Gold", colors: ["#fbbf24", "#ca8a04", "#92400e"], }, silver: { label: "Silver", colors: ["#e5e7eb", "#9ca3af", "#6b7280"], }, bronze: { label: "Bronze", colors: ["#d97706", "#92400e", "#78350f"], }, copper: { label: "Copper", colors: ["#f97316", "#c2410c", "#7c2d12"], }, darkSteel: { label: "Dark Steel", colors: ["#6b7280", "#4b5563", "#374151"], }, }; const RIBBON_ASPECT = 3.667; const RIBBON_WIDTH = 366; // ─── SVG Medal Shapes ─────────────────────────────────────────────── function MedalBody({ shape, finish, engraving, cx, cy, }: { shape: MedalShape; finish: MetalFinish; engraving: string; cx: number; cy: number; }) { const finishColors = FINISH_INFO[finish].colors; const gradId = `medal-grad-${finish}`; const r = 40; return ( {shape === "circle" && ( )} {shape === "star" && ( { const angle = Math.PI / 2 + (i * Math.PI) / 5; const radius = i % 2 === 0 ? r : r * 0.45; return `${cx + radius * Math.cos(angle)},${cy - radius * Math.sin(angle)}`; }).join(" ")} fill={`url(#${gradId})`} stroke={finishColors[2]} strokeWidth={1.5} /> )} {shape === "cross" && ( )} {shape === "shield" && ( )} {engraving && ( 10 ? 6 : 8} fontFamily="serif" fontWeight="bold" letterSpacing={0.5} > {engraving.toUpperCase()} )} ); } // ─── Preview ──────────────────────────────────────────────────────── function MedalPreview({ design }: { design: MedalDesign }) { const { ribbonStripes, shape, finish, engraving } = design; const totalWeight = ribbonStripes.reduce((s, stripe) => s + stripe.width, 0); const viewWidth = RIBBON_WIDTH; const ribbonTop = 14; const ribbonBottom = 72; const convergeRatio = 0.35; const medalCx = viewWidth / 2; const medalCy = 120; const ribbonViewWidth = RIBBON_WIDTH; const offset = 0; return ( {/* Ribbon as upside-down triangle (V-fold) */} {(() => { const topStartX = offset; const bottomCenter = medalCx; const topWidth = ribbonViewWidth; const bottomWidth = topWidth * convergeRatio; let topX = topStartX; return ribbonStripes.map((stripe, i) => { const stripeW = (stripe.width / totalWeight) * ribbonViewWidth; const fracStart = (topX - topStartX) / topWidth; const fracEnd = (topX + stripeW - topStartX) / topWidth; const tlx = topX; const trx = topX + stripeW; const blx = bottomCenter - bottomWidth / 2 + fracStart * bottomWidth; const brx = bottomCenter - bottomWidth / 2 + fracEnd * bottomWidth; topX += stripeW; return ( ); }); })()} {/* Small connector tab between ribbon and medal */} {/* Medal body */} ); } // ─── Stripe Editor (shared pattern) ───────────────────────────────── function StripeEditor({ stripes, onChange, }: { stripes: Stripe[]; onChange: (s: Stripe[]) => void; }) { const totalWeight = stripes.reduce((sum, s) => sum + s.width, 0); const addStripe = () => onChange([...stripes, { color: "#f5f5f4", width: 10 }]); const removeStripe = (idx: number) => onChange(stripes.filter((_, i) => i !== idx)); const updateStripe = (idx: number, patch: Partial) => onChange(stripes.map((s, i) => (i === idx ? { ...s, ...patch } : s))); return (
Ribbon Stripes
{stripes.map((stripe, idx) => (
updateStripe(idx, { color: e.target.value })} className="size-7 rounded cursor-pointer bg-transparent border-0 p-0" /> updateStripe(idx, { color: e.target.value })} className="text-[11px] font-mono bg-zinc-800 border border-zinc-700 rounded px-2 py-1 w-20 text-zinc-300" />
updateStripe(idx, { width: Number(e.target.value) })} className="flex-1 h-1 accent-zinc-400" /> {totalWeight > 0 ? Math.round((stripe.width / totalWeight) * 100) : 0}%
))}
); } // ─── Shape Selector ───────────────────────────────────────────────── function ShapeSelector({ value, onChange, }: { value: MedalShape; onChange: (s: MedalShape) => void; }) { return (
Medal Shape
{(Object.keys(SHAPE_INFO) as MedalShape[]).map((shape) => { const info = SHAPE_INFO[shape]; const active = value === shape; return ( ); })}
); } // ─── Finish Selector ──────────────────────────────────────────────── function FinishSelector({ value, onChange, }: { value: MetalFinish; onChange: (f: MetalFinish) => void; }) { return (
Metal Finish
{(Object.keys(FINISH_INFO) as MetalFinish[]).map((finish) => { const info = FINISH_INFO[finish]; const active = value === finish; return ( ); })}
); } // ─── Main Designer ────────────────────────────────────────────────── export default function MedalDesigner() { const { value, setValue } = useField(); const initial: MedalDesign = value && typeof value === "object" && Array.isArray((value as MedalDesign).ribbonStripes) ? (value as MedalDesign) : DEFAULT_MEDAL; const [design, setDesign] = useState(initial); const lastSaved = useRef(""); useEffect(() => { const serialized = JSON.stringify(design); if (serialized !== lastSaved.current) { lastSaved.current = serialized; setValue(design); } }, [design, setValue]); const setStripes = useCallback( (stripes: Stripe[]) => setDesign((d) => ({ ...d, ribbonStripes: stripes })), [], ); const setShape = useCallback((shape: MedalShape) => setDesign((d) => ({ ...d, shape })), []); const setFinish = useCallback((finish: MetalFinish) => setDesign((d) => ({ ...d, finish })), []); const setEngraving = useCallback( (engraving: string) => setDesign((d) => ({ ...d, engraving })), [], ); return (

Configure the medal shape, metal finish, ribbon, and optional engraving.

Engraving setEngraving(e.target.value)} maxLength={20} placeholder="e.g. VALOR" className="text-sm bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-zinc-300 placeholder:text-zinc-600" /> Short text engraved on the medal face (max 20 chars)
); }