/* 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 ? (

) : (
+
)}
{value && (
)}
);
}
function Stars({ value, onChange }) {
return (
{[1,2,3,4,5].map(n => (
))}
);
}
function StatEditor({ stat, onChange }) {
const [iconOpen, setIconOpen] = useState(false);
return (
{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 (
);
}
// ============ Stage (center preview) ============
function Stage({ card, toolbar, showAllRarities, allRaritiesData }) {
if (showAllRarities && allRaritiesData) {
return (
{toolbar}
{RARITY_LIST.map(r => (
))}
);
}
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) */}
{toast && {toast}
}
>
);
}
ReactDOM.createRoot(document.getElementById("root")).render();