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: '', power: '', toughness: '', frame_id: 'classic', artwork_url: '', }; 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.

{message && (
{message.text}
)}
{/* ── Form column ─────────────────────────────── */}
{/* Frame picker */}

Frame

{FRAMES.map((frame) => ( ))}
{/* Artwork */}

Artwork

handleUpload(e.target.files?.[0])} />
{design.artwork_url && ( )} 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) => ( ))}