/* global React, ReactDOM, htmlToImage, JSZip, Figurinha, RARITIES, ICON_NAMES, Icons */ const { useState, useEffect, useRef, useMemo, useCallback } = React; // ============ Defaults & types ============ const TYPES = [ { id: "camisa", label: "Camisa" }, { id: "jogador", label: "Jogador" }, { id: "estadio", label: "Estádio" }, { id: "momento", label: "Momento" }, { id: "taca", label: "Taça" }, { id: "campeoes", label: "Campeões" }, { id: "foto", label: "Foto" }, ]; const RARITY_LIST = ["comum", "raro", "epico", "lendario"]; // stat presets per type — sensible defaults the user can override const STAT_PRESETS = { camisa: [ { icon: "trophy", label: "MUNDIAIS", value: "5" }, { icon: "star", label: "TÍTULOS", value: "5" }, { icon: "ball", label: "GOLS MARCADOS", value: "229" }, ], jogador: [ { icon: "position", label: "POSIÇÃO", value: "ATAC" }, { icon: "calendar", label: "ESTREIA", value: "1976" }, { icon: "ball", label: "GOLS", value: "62" }, ], estadio: [ { icon: "people", label: "CAPACIDADE", value: "99.354" }, { icon: "calendar", label: "FUNDADO", value: "1957" }, { icon: "flag", label: "PAÍS", value: "BRA" }, ], momento: [ { icon: "trophy", label: "PLACAR", value: "2-0" }, { icon: "calendar", label: "ANO", value: "2002" }, { icon: "shield", label: "FASE", value: "FINAL" }, ], taca: [ { icon: "trophy", label: "EDIÇÃO", value: "2002" }, { icon: "crown", label: "CAMPEÃO", value: "BRA" }, { icon: "star", label: "ESTRELAS", value: "5" }, ], campeoes: [ { icon: "trophy", label: "VITÓRIAS", value: "7" }, { icon: "ball", label: "GOLS", value: "18" }, { icon: "shield", label: "SOFRIDOS", value: "4" }, ], foto: [ { icon: "people", label: "CAPACIDADE:", value: "72.327 PESSOAS" }, { icon: "calendar", label: "INAUGURAÇÃO:", value: "1998" }, { icon: "trophy", label: "FINAL DA COPA DO MUNDO 2002", value: "BRASIL 2 X 0 ALEMANHA" }, { icon: "ball", label: "GOLS DO FENÔMENO", value: "R9 FEZ OS 2 GOLS DA FINAL" }, ], }; // per-type defaults for layout flags const TYPE_DEFAULTS = { camisa: { photoLayout: false, hideFlag: false }, jogador: { photoLayout: false, hideFlag: false }, estadio: { photoLayout: false, hideFlag: false }, momento: { photoLayout: false, hideFlag: false }, taca: { photoLayout: false, hideFlag: false }, campeoes: { photoLayout: false, hideFlag: false }, foto: { photoLayout: true, hideFlag: true }, }; function makeDefault(type = "camisa", rarity = "comum") { const defaultName = { camisa: "BRASIL", jogador: "FENÔMENO", estadio: "MARACANÃ", momento: "GOL DO REI", taca: "TAÇA", campeoes: "CAMPEÕES", foto: "INTERNATIONAL STADIUM YOKOHAMA", }[type] || "NOVA"; const defaultSubtitle = type === "foto" ? "YOKOHAMA, JAPÃO" : ""; const d = TYPE_DEFAULTS[type] || { photoLayout: false, hideFlag: false }; return { id: Math.random().toString(36).slice(2, 10), type, rarity, name: defaultName, subtitle: defaultSubtitle, image: null, flag: null, stars: 5, stats: STAT_PRESETS[type].map(s => ({ ...s })), photoLayout: d.photoLayout, hideFlag: d.hideFlag, }; } // ============ LocalStorage ============ const STORAGE_KEY = "figurinhas-v1"; function loadLib() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; return JSON.parse(raw); } catch (e) { return null; } } function saveLib(state) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) { /* quota */ } } // ============ File → dataURL ============ function fileToDataURL(file) { return new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(r.result); r.onerror = reject; r.readAsDataURL(file); }); } // ============ Export ============ async function exportCardPng(node, filename) { // node is the .card-frame element (400x512) const dataUrl = await htmlToImage.toPng(node, { width: 400, height: 512, pixelRatio: 1, cacheBust: true, backgroundColor: null, style: { transform: "none" }, }); const a = document.createElement("a"); a.href = dataUrl; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } async function exportAllZip(cards) { const zip = new JSZip(); for (let i = 0; i < cards.length; i++) { const c = cards[i]; const node = document.getElementById(`exp-${c.id}`); if (!node) continue; const dataUrl = await htmlToImage.toPng(node, { width: 400, height: 512, pixelRatio: 1, cacheBust: true, backgroundColor: null, style: { transform: "none" }, }); const blob = await (await fetch(dataUrl)).blob(); const safe = c.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""); zip.file(`${safe || "card"}_${c.rarity}_${c.type}.png`, blob); } const out = await zip.generateAsync({ type: "blob" }); const url = URL.createObjectURL(out); const a = document.createElement("a"); a.href = url; a.download = `figurinhas_${new Date().toISOString().slice(0,10)}.zip`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } // ============ Components ============ function Segmented({ value, onChange, options, columns }) { const colClass = columns ? `seg seg-${columns}` : "seg"; return (
{options.map(opt => ( ))}
); } function IconPicker({ value, onChange }) { return (
{ICON_NAMES.map(name => ( ))}
); } function Upload({ value, onChange, label, sublabel }) { const inputRef = useRef(null); const onPick = async e => { const file = e.target.files?.[0]; if (!file) return; const dataUrl = await fileToDataURL(file); onChange(dataUrl); }; return (
inputRef.current?.click()} > {value ? ( ) : (
)}
{label}
{sublabel}
{value && ( )}
); } function Stars({ value, onChange }) { return (
{[1,2,3,4,5].map(n => ( ))}
); } function StatEditor({ stat, onChange }) { const [iconOpen, setIconOpen] = useState(false); return (
onChange({ ...stat, label: e.target.value.toUpperCase() })} /> onChange({ ...stat, value: e.target.value })} />
{iconOpen && ( { onChange({ ...stat, icon: name }); setIconOpen(false); }} /> )}
); } // ============ Library (left sidebar) ============ function Library({ cards, activeId, onSelect, onAdd, onDelete }) { return ( ); } // ============ Editor (right sidebar) ============ function Editor({ card, onChange, onDuplicate, onExport }) { if (!card) { return ( ); } const update = patch => onChange({ ...card, ...patch }); return ( ); } // ============ Auto-scaling wrapper ============ function CardScaler({ children, baseWidth = 400, baseHeight = 512, padding = 48 }) { const wrapRef = useRef(null); const [scale, setScale] = useState(1); useEffect(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(entries => { for (const e of entries) { const w = e.contentRect.width; const h = e.contentRect.height; const sx = (w - padding) / baseWidth; const sy = (h - padding) / baseHeight; const s = Math.min(1, Math.max(0.2, Math.min(sx, sy))); setScale(s); } }); ro.observe(wrapRef.current); return () => ro.disconnect(); }, [baseWidth, baseHeight, padding]); return (
{children}
); } // ============ Stage (center preview) ============ function Stage({ card, toolbar, showAllRarities, allRaritiesData }) { if (showAllRarities && allRaritiesData) { return (
{toolbar}
{RARITY_LIST.map(r => (
{RARITIES[r].label}
))}
); } if (!card) { return (
{toolbar}
🃏
Crie sua primeira figurinha
Configure raridade, tipo, arte central e estatísticas. Exporte como PNG 400×512.
); } return (
{toolbar}
{card.name.toUpperCase()} · {RARITIES[card.rarity].label} · 400×512 PNG
); } // ============ Stage toolbar (inline, not floating) ============ function StageToolbar({ cards, onExportAll, onShowAllRarities, onWipe, showAllRarities }) { return (
); } // ============ App ============ function App() { const [cards, setCards] = useState(() => { const loaded = loadLib(); if (loaded?.cards?.length) return loaded.cards; return [makeDefault("camisa", "comum")]; }); const [activeId, setActiveId] = useState(() => { const loaded = loadLib(); return loaded?.activeId || null; }); const [toast, setToast] = useState(null); const [showAllRarities, setShowAllRarities] = useState(false); // initialize activeId if absent useEffect(() => { if (!activeId && cards.length) setActiveId(cards[0].id); }, [activeId, cards]); // persist useEffect(() => { saveLib({ cards, activeId }); }, [cards, activeId]); const active = useMemo(() => cards.find(c => c.id === activeId), [cards, activeId]); const flashToast = msg => { setToast(msg); setTimeout(() => setToast(null), 2200); }; const handleChange = updated => { setCards(cs => cs.map(c => c.id === updated.id ? updated : c)); }; const handleAdd = () => { const last = cards[cards.length - 1]; const seed = last ? { ...last, id: Math.random().toString(36).slice(2, 10), name: "NOVA" } : makeDefault(); setCards(cs => [...cs, seed]); setActiveId(seed.id); }; const handleDuplicate = () => { if (!active) return; const copy = { ...active, id: Math.random().toString(36).slice(2, 10), name: active.name + " ·2" }; setCards(cs => [...cs, copy]); setActiveId(copy.id); flashToast("Figurinha duplicada"); }; const handleDelete = id => { setCards(cs => cs.filter(c => c.id !== id)); if (activeId === id) setActiveId(null); }; const handleExport = async () => { if (!active) return; const node = document.getElementById(`exp-${active.id}`); if (!node) return; const safe = active.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""); try { flashToast("Exportando…"); await exportCardPng(node, `${safe || "card"}_${active.rarity}_${active.type}.png`); flashToast("PNG salvo"); } catch (e) { flashToast("Erro ao exportar"); console.error(e); } }; const handleExportAll = async () => { try { flashToast(`Exportando ${cards.length} figurinhas…`); await exportAllZip(cards); flashToast("ZIP gerado"); } catch (e) { flashToast("Erro ao exportar todas"); console.error(e); } }; const handleWipe = () => { if (!confirm("Limpar TODA a biblioteca? Esta ação não pode ser desfeita.")) return; setCards([makeDefault("camisa", "comum")]); setActiveId(null); }; return ( <>
setShowAllRarities(v => !v)} onWipe={handleWipe} showAllRarities={showAllRarities} /> } />
{/* Hidden export nodes (always rendered at exact 400x512 with no transforms) */}
{cards.map(c => (
))}
{toast &&
{toast}
} ); } ReactDOM.createRoot(document.getElementById("root")).render();