// ============================================================ // Made-to-order commission wizard - FreshWave Digital Studio // ============================================================ const COMMISSION_STEPS = ['Category', 'Material', 'Finish', 'Scope', 'Details']; function StepDots({ step }) { return (
{COMMISSION_STEPS.map((label, idx) => { const done = idx < step; const active = idx === step; return (
{done ? : idx + 1} {label}
{idx < COMMISSION_STEPS.length - 1 && }
); })}
); } // Selectable card function OptionCard({ selected, onClick, accent = 'cyan', title, note, swatch }) { return ( ); } const START_LANES = [ { id: 'preloads', eyebrow: 'Sleeper upgrades', title: 'Sleeper Items', accent: 'cyan', body: 'Small quality-of-life prints people do not expect, but use every day once they have them.', points: ['Desk and shelf upgrades', 'Practical gift energy', 'Fastest quote path'], action: 'Browse sleeper items', }, { id: 'custom', eyebrow: 'One of one', title: 'Custom Request', accent: 'magenta', body: 'Bring the sketch, screenshot, reference photo or half-formed idea. We will scope it by hand.', points: ['Original characters', 'Replicas and repairs', 'Odd ideas welcome'], action: 'Build a brief', }, ]; function StartPieceGateway({ onChoose }) { return (
Start a piece

Choose your lane.

Pick a sleeper item when you want a useful FreshWave upgrade with fast options. Go custom when the idea needs a sculpt, replica, remix or deeper quote.

{START_LANES.map((lane) => ( onChoose(lane.id)} /> ))}
01 / Pick
Pick a sleeper item or describe the piece from scratch.
02 / Scope
Choose material, finish, size and the notes that matter.
03 / Quote
We confirm price, deadline and production details within 72 hours.
); } function LaneCard({ lane, onChoose }) { return ( ); } function PreloadFlow({ onBack, backLabel = 'Start a Piece' }) { const [selectedId, setSelectedId] = useState(PRELOADS[0].id); const [finish, setFinish] = useState('white'); const [size, setSize] = useState('standard'); const [brief, setBrief] = useState({ name: '', email: '', notes: '' }); const [sent, setSent] = useState(false); const [sending, setSending] = useState(false); const [fallbackMailto, setFallbackMailto] = useState(''); const [ticket, setTicket] = useState(''); const selected = PRELOADS.find((item) => item.id === selectedId) || PRELOADS[0]; const selectedFinish = PRELOAD_FINISHES.find((item) => item.id === finish) || PRELOAD_FINISHES[0]; const selectedSize = PRELOAD_SIZES.find((item) => item.id === size) || PRELOAD_SIZES[0]; const sizeAdd = preloadSizeAdjustment(selected, selectedSize); const price = preloadEstimate(selected, selectedFinish, selectedSize); const setBriefValue = (key, value) => setBrief((current) => ({ ...current, [key]: value })); const canSend = brief.name.trim() && /\S+@\S+\.\S+/.test(brief.email); const submit = async (event) => { event.preventDefault(); if (!canSend) return; setSending(true); setFallbackMailto(''); setTicket(''); const payload = { kind: 'preload', name: brief.name, email: brief.email, preload: selected.title, category: selected.category, finish: selectedFinish.label, size: selectedSize.label, estimate: `$${price}`, message: brief.notes || `Requesting ${selected.title} with ${selectedFinish.label} at ${selectedSize.label}.`, notes: brief.notes, }; try { const result = await submitInquiry(payload); setTicket(result.ticket || ''); setSent(true); } catch (error) { const mailto = buildInquiryMailto(payload); setFallbackMailto(mailto); window.location.href = mailto; setSent(true); } finally { setSending(false); } }; if (sent) { return (

Sleeper item request queued.

Thanks {brief.name.split(' ')[0] || 'there'} - we logged the {selected.title} with {selectedFinish.label.toLowerCase()} at {selectedSize.label.toLowerCase()}.

Selected total: ${price}

{ticket &&

Ticket {ticket}

} {fallbackMailto && (
The server form was unavailable, so we opened a backup email draft with your sleeper item details. Open email draft
)}
); } return (
Sleeper items

Pick the useful one.

Unexpected quality-of-life prints with known shapes, faster timelines and enough options to make them feel yours.

{PRELOADS.map((item) => ( setSelectedId(item.id)} /> ))}
Order brief

{selected.title}

Current total
${price}
{selected.description}
Price check
{selected.title}${selected.price}
{selectedSize.label}{sizeAdd ? `+$${sizeAdd}` : 'included'}
{selectedFinish.label}{selectedFinish.price ? `+$${selectedFinish.price}` : 'included'}
{PRELOAD_SIZES.map((item) => ( ))}
{PRELOAD_FINISHES.map((item) => ( ))}
setBriefValue('name', e.target.value)} placeholder="Maya Chen" className="fw-input" /> setBriefValue('email', e.target.value)} placeholder="maya@example.com" className="fw-input" />

No payment collected here. Final quote confirmed by hand.

); } function PreloadCard({ item, selected, onSelect }) { return ( ); } function preloadEstimate(item, finish, size) { return item.price + finish.price + preloadSizeAdjustment(item, size); } function preloadSizeAdjustment(item, size) { return Math.max(0, Math.ceil(item.price * ((size.mult || 1) - 1))); } function CommissionPage() { const [mode, setMode] = useState('start'); useEffect(() => { window.scrollTo(0, 0); }, [mode]); if (mode === 'preloads') return setMode('start')} />; if (mode === 'custom') return setMode('start')} />; return ; } function SleepersPage() { const sleeperLane = { ...START_LANES.find((lane) => lane.id === 'preloads'), eyebrow: 'Fundamentals / quality of life', title: 'Sleeper Picks', body: 'The little prints that have made a surprisingly good difference in my own day-to-day setup. Easy to overlook at first, then hard to go without.', points: ['Everyday setup wins', 'Small details that help', 'Built for the daily rotation'], action: 'See the sleeper catalogue', }; return (
Sleeper picks

Small upgrades.
Big everyday difference.

These are the pieces that surprised me: the simple, reliable additions that make a desk, shelf or daily routine feel a little more dialed-in every time I use them.

The FreshWave read

The ones that quietly earn a place.

Sleeper Picks are not trying to replace the big statement pieces. They are the low-key additions that have made my own everyday life smoother, more organized and just more enjoyable to use. Some work quietly in the background - like the Cable Comb Kit giving a crowded desk room to breathe - but that is exactly why they earn their place.

go('/sleepers/catalogue')} />
); } function SleepersCataloguePage() { return go('/sleepers')} backLabel="Sleeper Picks" />; } function CustomRequestFlow({ onBack }) { const [step, setStep] = useState(0); const stepCardRef = useRef(null); const didMountStepRef = useRef(false); const [data, setData] = useState({ category: '', material: '', finish: '', qty: 1, size: 'Palm (≤3in)', notes: '', name: '', email: '', }); const [done, setDone] = useState(false); const [sending, setSending] = useState(false); const [fallbackMailto, setFallbackMailto] = useState(''); const [ticket, setTicket] = useState(''); const set = (patch) => setData((d) => ({ ...d, ...patch })); const sizes = ['Palm (≤3in)', 'Desk (3-6in)', 'Shelf (6-9in)', 'Statue (up to 12in)']; useEffect(() => { if (!didMountStepRef.current) { didMountStepRef.current = true; return; } window.requestAnimationFrame(() => { if (!stepCardRef.current) return; const prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; const stickyHeaderOffset = 92; const top = stepCardRef.current.getBoundingClientRect().top + window.scrollY - stickyHeaderOffset; window.scrollTo({ top: Math.max(0, top), behavior: prefersReducedMotion ? 'auto' : 'smooth' }); }); }, [step]); const canNext = [ () => !!data.category, () => !!data.material, () => !!data.finish, () => !!data.size && data.qty >= 1, () => data.name.trim() && /\S+@\S+\.\S+/.test(data.email), ][step]; const next = async () => { if (step < COMMISSION_STEPS.length - 1) { setStep(step + 1); return; } if (!canNext()) return; const catLabel = (COMMISSION_CATEGORIES.find((c) => c.id === data.category) || {}).label || ''; const matLabel = (COMMISSION_MATERIALS.find((m) => m.id === data.material) || {}).label || ''; const finLabel = (COMMISSION_FINISHES.find((f) => f.id === data.finish) || {}).label || ''; const estimate = estimateRange(data); const payload = { kind: 'custom', name: data.name, email: data.email, category: catLabel, material: matLabel, finish: finLabel, size: data.size, quantity: data.qty, estimate: estimate ? `$${estimate.low}-${estimate.high}` : '', message: data.notes || `Custom request for ${catLabel || 'a FreshWave piece'}.`, notes: data.notes, }; setSending(true); setFallbackMailto(''); setTicket(''); try { const result = await submitInquiry(payload); setTicket(result.ticket || ''); setDone(true); } catch (error) { const mailto = buildInquiryMailto(payload); setFallbackMailto(mailto); window.location.href = mailto; setDone(true); } finally { setSending(false); } }; const back = () => { if (step > 0) setStep(step - 1); }; if (done) { const catLabel = (COMMISSION_CATEGORIES.find((c) => c.id === data.category) || {}).label; const matLabel = (COMMISSION_MATERIALS.find((m) => m.id === data.material) || {}).label; return (

Brief received.

Thanks {data.name.split(' ')[0]}. We've logged your {catLabel ? catLabel.toLowerCase() : 'piece'} in {matLabel || 'your chosen material'}. We'll come back within 72 hours with a firm quote and next steps.

{estimateRange(data) && (

Working estimate: ${estimateRange(data).low} to ${estimateRange(data).high}

)} {ticket &&

Ticket {ticket}

} {fallbackMailto && (
The server form was unavailable, so we opened a backup email draft with your custom request. Open email draft
)}
); } return (
Made to order

Custom request

Five quick steps to a brief. No account, no commitment. Just enough for us to scope it and quote it.

{/* STEP 0 - category */} {step === 0 && (

What are we making?

Pick the closest fit. We'll figure out the details together.

{COMMISSION_CATEGORIES.map((c) => ( set({ category: c.id })} /> ))}
)} {/* STEP 1 - material */} {step === 1 && (

Pick a material

Each one prints, feels and finishes differently. Not sure? Let us recommend.

{COMMISSION_MATERIALS.map((m) => ( set({ material: m.id })} /> ))}
)} {/* STEP 2 - finish */} {step === 2 && (

Choose a finish

Keep it paintable, finish it in color, or add the FreshWave glow layer.

{COMMISSION_FINISHES.map((f) => ( set({ finish: f.id })} /> ))}
)} {/* STEP 3 - scope */} {step === 3 && (

Size & scope

Rough is fine. It just helps us scope the print time.

{sizes.map((s) => ( ))}
{data.qty} {data.qty === 1 ? 'piece' : 'pieces'}
)} {/* STEP 4 - details */} {step === 4 && (

Tell us about it

A sentence or a spec sheet. Whatever you've got. Attach files later by email.

set({ name: e.target.value })} placeholder="Maya Chen" className="fw-input" /> set({ email: e.target.value })} placeholder="maya@example.com" className="fw-input" />
)} {/* live estimate + nav */}
{estimateRange(data) && (
Estimated range
${estimateRange(data).low}to${estimateRange(data).high}
Ballpark only. Final quote confirmed within 72h.
)}
Step {step + 1} / {COMMISSION_STEPS.length}
); } function Field({ label, children }) { return (
{children}
); } function CommissionSummary({ data }) { const cat = (COMMISSION_CATEGORIES.find((c) => c.id === data.category) || {}).label || 'Not selected'; const mat = (COMMISSION_MATERIALS.find((m) => m.id === data.material) || {}).label || 'Not selected'; const fin = (COMMISSION_FINISHES.find((f) => f.id === data.finish) || {}).label || 'Not selected'; const rows = [['Category', cat], ['Material', mat], ['Finish', fin], ['Size', data.size], ['Quantity', String(data.qty)]]; return (
Your brief so far
{rows.map(([k, v]) => (
{k}
{v}
))}
); } Object.assign(window, { CommissionPage, SleepersPage, SleepersCataloguePage, Field });