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 (
Arrange cards on a US Letter sheet (63 × 88 mm cards, cut guides included).
{selectedDesigns.length} card{selectedDesigns.length === 1 ? '' : 's'} selected
{selectedDesigns.length > perSheet
? ` · ${sheetCount} sheets (export one at a time)`
: ''}
No designs yet — create some in the{' '}
designer first.
Print Sheet
Layout
Cards
{designs.length === 0 ? (
Sheet Preview