import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/router'; import Layout from '../../components/Layout'; import CardFrame, { CARD_W, CARD_H } from '../../components/designer/CardFrame'; import { Button } from '../../components/ui'; import { useAuth } from '../../lib/use-auth'; // US Letter at 300 DPI. Standard TCG card = 63 x 88 mm -> 744 x 1040 px. const SHEET_W = 2550; const SHEET_H = 3300; const CARD_PRINT_W = 744; const CARD_PRINT_H = 1040; const CARD_SCALE = CARD_PRINT_W / CARD_W; // 420 -> 744 const LAYOUTS = { '3x3': { cols: 3, rows: 3, label: '3 × 3 (9 cards)' }, '2x2': { cols: 2, rows: 2, label: '2 × 2 (4 cards)' }, }; export default function PrintSheet() { const router = useRouter(); const { user, loading: authLoading } = useAuth(); const [designs, setDesigns] = useState([]); const [symbolsMap, setSymbolsMap] = useState({}); const [loading, setLoading] = useState(true); const [selected, setSelected] = useState(new Set()); const [layout, setLayout] = useState('3x3'); const [exporting, setExporting] = useState(false); const [error, setError] = useState(null); const sheetRef = useRef(null); useEffect(() => { if (!authLoading && !user) { router.push('/login'); } }, [authLoading, user, router]); useEffect(() => { if (!user) return undefined; const load = async () => { try { const token = localStorage.getItem('auth_token'); const headers = token ? { Authorization: `Bearer ${token}` } : {}; const [cardsRes, symbolsRes] = await Promise.all([ fetch('/api/custom-cards', { headers }), fetch('/api/custom-symbols', { headers }), ]); if (cardsRes.ok) { const data = await cardsRes.json(); setDesigns(data.designs); // Preselect via ?ids=1,2,3 — otherwise everything. const idsParam = router.query.ids; if (typeof idsParam === 'string' && idsParam.length > 0) { const ids = new Set( idsParam.split(',').map((n) => parseInt(n, 10)).filter(Number.isInteger) ); setSelected(new Set(data.designs.filter((d) => ids.has(d.id)).map((d) => d.id))); } else { setSelected(new Set(data.designs.map((d) => d.id))); } } else { setError('Failed to load your designs.'); } if (symbolsRes.ok) { const data = await symbolsRes.json(); setSymbolsMap(Object.fromEntries(data.symbols.map((s) => [s.code, s.image_url]))); } } catch { setError('Failed to load your designs.'); } finally { setLoading(false); } }; load(); return undefined; }, [user, router.query.ids]); const toggle = (id) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const selectedDesigns = useMemo( () => designs.filter((d) => selected.has(d.id)), [designs, selected] ); // Preview scale so the full sheet fits the preview pane. const [previewScale, setPreviewScale] = useState(0.25); const previewWrapRef = useRef(null); const updatePreviewScale = useCallback(() => { const el = previewWrapRef.current; if (el) setPreviewScale(Math.min(0.35, el.clientWidth / SHEET_W)); }, []); useEffect(() => { updatePreviewScale(); const observer = new ResizeObserver(updatePreviewScale); if (previewWrapRef.current) observer.observe(previewWrapRef.current); return () => observer.disconnect(); }, [updatePreviewScale]); const handleExport = async () => { if (!sheetRef.current || selectedDesigns.length === 0) return; setExporting(true); setError(null); try { const { toPng } = await import('html-to-image'); const dataUrl = await toPng(sheetRef.current, { width: SHEET_W, height: SHEET_H, pixelRatio: 1, cacheBust: true, }); const link = document.createElement('a'); link.download = `deckhearth-print-sheet-${selectedDesigns.length}cards.png`; link.href = dataUrl; link.click(); } catch { setError('Export failed. Please try again.'); } finally { setExporting(false); } }; if (loading) { return (
); } const { cols, rows } = LAYOUTS[layout]; const perSheet = cols * rows; const slots = Array.from({ length: perSheet }, (_, i) => selectedDesigns[i] || null); const sheetCount = Math.max(1, Math.ceil(selectedDesigns.length / perSheet)); return (

Print Sheet

Arrange cards on a US Letter sheet (63 × 88 mm cards, cut guides included).

{error && (
{error}
)}
{/* Controls + card picker */}

Layout

{Object.entries(LAYOUTS).map(([key, cfg]) => ( ))}

{selectedDesigns.length} card{selectedDesigns.length === 1 ? '' : 's'} selected {selectedDesigns.length > perSheet ? ` · ${sheetCount} sheets (export one at a time)` : ''}

Cards

{designs.length === 0 ? (

No designs yet — create some in the{' '} designer first.

) : (
{designs.map((design) => ( ))}
)}
{/* Sheet preview */}

Sheet Preview

); } /** * The actual 2550x3300 print surface. Cards render at natural 420x588 * then scale up 1.771x to true print size; dashed guides mark cut lines. */ function PrintSheetSurface({ slots, cols, rows, sheetRef, symbolsMap }) { const marginX = (SHEET_W - cols * CARD_PRINT_W) / 2; const marginTop = (SHEET_H - rows * CARD_PRINT_H) / 2; return (
{slots.map((design, i) => { const col = i % cols; const row = Math.floor(i / cols); const x = marginX + col * CARD_PRINT_W; const y = marginTop + row * CARD_PRINT_H; return (
{design && (
)}
); })}
); }