import { useCallback, useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import Layout from '../components/Layout'; import CardPreview from '../components/designer/CardFrame'; import { FRAMES, RARITIES } from '../components/designer/frames'; import { Button } from '../components/ui'; import { useAuth } from '../lib/use-auth'; const BLANK_DESIGN = { id: null, name: '', mana_cost: '', card_type: '', rarity: 'common', rules_text: '', actions: '', flavor_quote: '', power: '', toughness: '', frame_id: 'classic', artwork_url: '', art_mode: 'framed', }; export default function Designer() { const router = useRouter(); const { user, loading: authLoading } = useAuth(); const [design, setDesign] = useState(BLANK_DESIGN); const [saving, setSaving] = useState(false); const [exporting, setExporting] = useState(false); const [uploading, setUploading] = useState(false); const [message, setMessage] = useState(null); const cardRef = useRef(null); const fileInputRef = useRef(null); // Edit mode when ?id= is present useEffect(() => { if (!user || !router.query.id) return; const loadDesign = async () => { try { const token = localStorage.getItem('auth_token'); const response = await fetch(`/api/custom-cards/${router.query.id}`, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); if (response.ok) { const data = await response.json(); setDesign({ ...BLANK_DESIGN, ...data.design }); } else { setMessage({ kind: 'error', text: 'Design not found.' }); } } catch { setMessage({ kind: 'error', text: 'Failed to load design.' }); } }; loadDesign(); }, [user, router.query.id]); const setField = useCallback((field, value) => { setDesign((prev) => ({ ...prev, [field]: value })); }, []); const handleUpload = async (file) => { if (!file) return; setUploading(true); setMessage(null); try { const body = new FormData(); body.append('artwork', file); const token = localStorage.getItem('auth_token'); const response = await fetch('/api/custom-cards/upload-artwork', { method: 'POST', headers: token ? { Authorization: `Bearer ${token}` } : undefined, body, }); const data = await response.json(); if (response.ok && data.artwork_url) { setField('artwork_url', data.artwork_url); } else { setMessage({ kind: 'error', text: data.error || 'Upload failed.' }); } } catch { setMessage({ kind: 'error', text: 'Upload failed. Please try again.' }); } finally { setUploading(false); } }; const handleSave = async () => { if (!design.name.trim()) { setMessage({ kind: 'error', text: 'Give your card a title first.' }); return; } setSaving(true); setMessage(null); try { const token = localStorage.getItem('auth_token'); const isNew = !design.id; const response = await fetch( isNew ? '/api/custom-cards' : `/api/custom-cards/${design.id}`, { method: isNew ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify(design), } ); const data = await response.json(); if (response.ok) { setDesign({ ...BLANK_DESIGN, ...data.design }); setMessage({ kind: 'success', text: isNew ? 'Card saved to your designs!' : 'Changes saved.', }); } else { setMessage({ kind: 'error', text: data.error || 'Save failed.' }); } } catch { setMessage({ kind: 'error', text: 'Save failed. Please try again.' }); } finally { setSaving(false); } }; const handleExportPng = async () => { if (!cardRef.current) return; setExporting(true); try { const { toPng } = await import('html-to-image'); const dataUrl = await toPng(cardRef.current, { width: 420, height: 588, pixelRatio: 2, cacheBust: true, }); const link = document.createElement('a'); link.download = `${(design.name || 'card').replace(/[^a-z0-9-_ ]/gi, '').trim() || 'card'}.png`; link.href = dataUrl; link.click(); } catch { setMessage({ kind: 'error', text: 'Export failed. Please try again.' }); } finally { setExporting(false); } }; const handleNew = () => { setDesign(BLANK_DESIGN); setMessage(null); router.replace('/designer', undefined, { shallow: true }); }; if (authLoading) { return ( ); } return ( {/* Header */} Card Designer Fill in the details and watch your card come to life. New {exporting ? 'Exporting…' : 'Download PNG'} {saving ? 'Saving…' : design.id ? 'Save Changes' : 'Save Card'} {message && ( {message.text} )} {/* ── Form column ─────────────────────────────── */} {/* Frame picker */} Frame {FRAMES.map((frame) => ( setField('frame_id', frame.id)} className={`rounded-xl p-3 text-left transition-all duration-200 border ${ design.frame_id === frame.id ? 'shadow-lg scale-[1.02]' : 'opacity-80 hover:opacity-100' }`} style={{ backgroundColor: frame.palette.outer, borderColor: design.frame_id === frame.id ? frame.palette.border : 'transparent', borderWidth: 2, }} aria-pressed={design.frame_id === frame.id} > {frame.name} ))} {/* Artwork */} Artwork handleUpload(e.target.files?.[0])} /> {[ { id: 'framed', label: 'Framed' }, { id: 'fullart', label: 'Full Art' }, ].map((mode) => ( setField('art_mode', mode.id)} className="px-4 py-2 text-xs font-semibold transition-all duration-200" style={{ backgroundColor: design.art_mode === mode.id ? 'var(--accent-ember)' : 'var(--bg-tertiary)', color: design.art_mode === mode.id ? 'white' : 'var(--text-secondary)', }} aria-pressed={design.art_mode === mode.id} > {mode.label} ))} fileInputRef.current?.click()} disabled={uploading}> {uploading ? 'Uploading…' : design.artwork_url ? 'Replace Image' : 'Upload Image'} {design.artwork_url && ( setField('artwork_url', '')}> Remove )} JPEG, PNG, or WebP · up to 5MB {/* Details */} Details setField('name', e.target.value)} placeholder="Emberwing Phoenix" maxLength={60} /> setField('mana_cost', e.target.value)} placeholder="2RR" maxLength={24} /> setField('card_type', e.target.value)} placeholder="Creature — Phoenix" maxLength={80} /> {RARITIES.map((rarity) => ( setField('rarity', rarity.id)} className={`flex-1 rounded-lg px-2 py-2 text-xs font-semibold transition-all duration-200 ${ design.rarity === rarity.id ? 'scale-105 shadow-md' : 'opacity-70 hover:opacity-100' }`} style={{ backgroundColor: 'var(--bg-tertiary)', color: design.rarity === rarity.id ? rarity.color : 'var(--text-secondary)', borderBottom: `3px solid ${rarity.color}`, }} aria-pressed={design.rarity === rarity.id} > {rarity.name} ))} setField('rules_text', e.target.value)} placeholder="What the card does, or what it describes…" maxLength={500} /> setField('actions', e.target.value)} placeholder={'Flying, haste\nWhen Emberwing enters the battlefield, it deals 2 damage to any target.'} maxLength={800} /> setField('flavor_quote', e.target.value)} placeholder="From the ashes, memory takes wing." maxLength={300} /> Power / Toughness (optional) setField('power', e.target.value)} placeholder="3" maxLength={10} /> setField('toughness', e.target.value)} placeholder="4" maxLength={10} /> {/* ── Live preview column ─────────────────────── */} Live Preview Saved designs appear in{' '} My Designs{' '} and your collection. ); } function Field({ label, hint, required, children }) { return ( {label} {required && *} {hint && ( ({hint}) )} {children} ); }
Fill in the details and watch your card come to life.
Saved designs appear in{' '} My Designs{' '} and your collection.