"use client"; import React, { useEffect, useMemo, useState } from 'react'; import type { TcgCard } from '@/types/pokemon'; import { loadChecklistServer, saveChecklistServer } from '@/lib/checklist'; import type { ChecklistV2, VariantKey } from '@/lib/checklist'; import CardGrid from '@/components/CardGrid'; import Header from '@/components/Header'; import SetFilter from '@/components/SetFilter'; export default function Page() { const [cards, setCards] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [query, setQuery] = useState(''); const [checklist, setChecklist] = useState({}); const [setId, setSetId] = useState(''); const [note, setNote] = useState(null); const [tab, setTab] = useState<'uncollected' | 'collected'>('uncollected'); const [refreshing, setRefreshing] = useState(false); const [updatedAt, setUpdatedAt] = useState(null); const [cachedFlag, setCachedFlag] = useState(null); const [toast, setToast] = useState(null); const [overrides, setOverrides] = useState>({}); useEffect(() => { (async () => { const data = await loadChecklistServer(); setChecklist(data); })(); }, []); // Load reverse overrides at start useEffect(() => { (async () => { try { const res = await fetch('/api/magikarp/override', { cache: 'no-store' }); if (!res.ok) return; const json = await res.json(); const map = (json?.data?.reverseHolofoil || {}) as Record; if (map && typeof map === 'object') setOverrides(map); } catch {} })(); // Listen for UI-initiated override changes function onOverrideChanged(ev: Event) { try { const ce = ev as CustomEvent<{ id: string; reverse: boolean }>; const { id, reverse } = ce.detail || ({} as any); if (!id) return; setOverrides((prev) => ({ ...prev, [id]: !!reverse })); } catch {} } window.addEventListener('reverse-override-changed', onOverrideChanged as EventListener); return () => window.removeEventListener('reverse-override-changed', onOverrideChanged as EventListener); }, []); useEffect(() => { async function load() { setLoading(true); setError(null); try { const res = await fetch('/api/magikarp?pageSize=250'); if (!res.ok) throw new Error('Failed to fetch'); const json = await res.json(); const list: TcgCard[] = json.data || []; list.sort((a, b) => { const da = Date.parse(a.set.releaseDate || ''); const db = Date.parse(b.set.releaseDate || ''); if (Number.isFinite(da) && Number.isFinite(db)) { if (da !== db) return da - db; // oldest first } else if (Number.isFinite(da)) { return -1; } else if (Number.isFinite(db)) { return 1; } // tie-breaker by card number if numeric const na = parseInt(a.number, 10); const nb = parseInt(b.number, 10); if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb; return a.number.localeCompare(b.number); }); setCards(list); if (json.note) setNote(String(json.note)); if (json.updatedAt) setUpdatedAt(String(json.updatedAt)); if (typeof json.cached === 'boolean') setCachedFlag(json.cached); } catch (e: any) { setError(e?.message || 'Error'); } finally { setLoading(false); } } load(); }, []); async function refreshCards() { setRefreshing(true); setError(null); try { const res = await fetch('/api/magikarp?pageSize=250&refresh=1', { cache: 'no-store' }); if (!res.ok) throw new Error('Failed to refresh'); const json = await res.json(); const list: TcgCard[] = json.data || []; list.sort((a, b) => { const da = Date.parse(a.set.releaseDate || ''); const db = Date.parse(b.set.releaseDate || ''); if (Number.isFinite(da) && Number.isFinite(db)) { if (da !== db) return da - db; // oldest first } else if (Number.isFinite(da)) { return -1; } else if (Number.isFinite(db)) { return 1; } const na = parseInt(a.number, 10); const nb = parseInt(b.number, 10); if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb; return a.number.localeCompare(b.number); }); setCards(list); if (json.note) setNote(String(json.note)); else setNote(null); if (json.updatedAt) setUpdatedAt(String(json.updatedAt)); if (typeof json.cached === 'boolean') setCachedFlag(json.cached); } catch (e: any) { // Non-blocking toast on refresh failure const msg = e?.message ? String(e.message) : 'Refresh failed'; setToast(msg); // auto-hide setTimeout(() => setToast(null), 3000); } finally { setRefreshing(false); } } function toggleCollected(id: string, key: VariantKey) { setChecklist((prev) => { const next: ChecklistV2 = { ...prev }; const current = { ...(next[id] || {}) }; current[key] = !current[key]; // Clean up empty records to keep storage tidy const hasAny = Object.values(current).some(Boolean); if (hasAny) next[id] = current; else delete next[id]; // fire-and-forget save to server; fallback handled in lib void saveChecklistServer(next); return next; }); } function availableVariants(card: TcgCard): VariantKey[] { const list: VariantKey[] = ['base']; const hasHolo = !!(card.variants?.holofoil || (card as any).tcgplayer?.prices?.holofoil); const hasReverse = !!(card.variants?.reverseHolofoil || (card as any).tcgplayer?.prices?.reverseHolofoil) || !!overrides[card.id]; if (hasHolo) list.push('holofoil'); if (hasReverse) list.push('reverseHolofoil'); return list; } function isFullyCollected(card: TcgCard): boolean { const needed = availableVariants(card); const state = checklist[card.id] || {}; return needed.every((k) => !!state[k]); } const filtered = useMemo(() => { const byQuery = (() => { if (!query.trim()) return cards; const q = query.toLowerCase(); return cards.filter((c) => c.name.toLowerCase().includes(q) || c.number.toLowerCase().includes(q) || (c.rarity || '').toLowerCase().includes(q) || c.set.name.toLowerCase().includes(q) || c.set.series.toLowerCase().includes(q) ); })(); const bySet = setId ? byQuery.filter((c) => c.set.id === setId) : byQuery; return bySet; }, [cards, query, setId]); // Variant-based counts for filtered set const filteredCollectedVariantsCount = useMemo(() => { return filtered.reduce((sum, c) => { const state = checklist[c.id] || {}; return sum + availableVariants(c).reduce((acc, k) => acc + (state[k] ? 1 : 0), 0); }, 0); }, [filtered, checklist]); const filteredTotalVariants = useMemo(() => filtered.reduce((sum, c) => sum + availableVariants(c).length, 0), [filtered]); const filteredUncollectedVariantsCount = useMemo(() => filteredTotalVariants - filteredCollectedVariantsCount, [filteredTotalVariants, filteredCollectedVariantsCount]); const displayed = useMemo(() => { return tab === 'collected' ? filtered.filter((c) => isFullyCollected(c)) : filtered.filter((c) => !isFullyCollected(c)); }, [filtered, checklist, tab]); const setOptions = useMemo(() => { const map = new Map(); for (const c of cards) { if (!map.has(c.set.id)) map.set(c.set.id, `${c.set.series} — ${c.set.name}`); } return Array.from(map.entries()).map(([id, label]) => ({ id, label })).sort((a, b) => a.label.localeCompare(b.label)); }, [cards]); // Overall variant-based totals for header const totalVariantsAll = useMemo(() => cards.reduce((sum, c) => sum + availableVariants(c).length, 0), [cards]); const collectedVariantsAll = useMemo(() => cards.reduce((sum, c) => { const state = checklist[c.id] || {}; return sum + availableVariants(c).reduce((acc, k) => acc + (state[k] ? 1 : 0), 0); }, 0), [cards, checklist]); return (
{updatedAt ? ( Updated: {new Date(updatedAt).toLocaleString()} {cachedFlag !== null && ( {cachedFlag ? 'Cached' : 'Live'} )} ) : null}
{loading ? (

Loading Magikarp cards…

) : error ? (

{error}

) : ( <> {note ? (
{note}
) : null}
Showing {displayed.length} of {filtered.length} cards in this tab.
{/* Toast */} {toast ? (
{toast}
) : null} )}
); }