diff --git a/src/app/page.tsx b/src/app/page.tsx index df7694e..2d5eced 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import type { TcgCard } from '@/types/pokemon'; import { loadChecklist, saveChecklist } 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'; @@ -11,13 +12,13 @@ export default function Page() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [query, setQuery] = useState(''); - const [collected, setCollected] = useState>(new Set()); + const [checklist, setChecklist] = useState({}); const [setId, setSetId] = useState(''); const [note, setNote] = useState(null); const [tab, setTab] = useState<'uncollected' | 'collected'>('uncollected'); useEffect(() => { - setCollected(loadChecklist()); + setChecklist(loadChecklist()); }, []); useEffect(() => { @@ -39,15 +40,34 @@ export default function Page() { load(); }, []); - function toggleCollected(id: string) { - setCollected((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); else next.add(id); + 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]; saveChecklist(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); + 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; @@ -64,14 +84,14 @@ export default function Page() { return bySet; }, [cards, query, setId]); - const filteredCollectedCount = useMemo(() => filtered.filter((c) => collected.has(c.id)).length, [filtered, collected]); - const filteredUncollectedCount = useMemo(() => filtered.filter((c) => !collected.has(c.id)).length, [filtered, collected]); + const filteredCollectedCount = useMemo(() => filtered.filter((c) => isFullyCollected(c)).length, [filtered, checklist]); + const filteredUncollectedCount = useMemo(() => filtered.length - filteredCollectedCount, [filtered, filteredCollectedCount]); const displayed = useMemo(() => { return tab === 'collected' - ? filtered.filter((c) => collected.has(c.id)) - : filtered.filter((c) => !collected.has(c.id)); - }, [filtered, collected, tab]); + ? filtered.filter((c) => isFullyCollected(c)) + : filtered.filter((c) => !isFullyCollected(c)); + }, [filtered, checklist, tab]); const setOptions = useMemo(() => { const map = new Map(); @@ -87,7 +107,7 @@ export default function Page() { query={query} onQueryChange={setQuery} total={cards.length} - collectedCount={collected.size} + collectedCount={useMemo(() => cards.filter((c) => isFullyCollected(c)).length, [cards, checklist])} />
@@ -138,7 +158,7 @@ export default function Page() {
Showing {displayed.length} of {filtered.length} cards in this tab.
- + )} diff --git a/src/components/CardGrid.tsx b/src/components/CardGrid.tsx index ed3354c..a8381f3 100644 --- a/src/components/CardGrid.tsx +++ b/src/components/CardGrid.tsx @@ -2,15 +2,16 @@ import React from 'react'; import type { TcgCard } from '@/types/pokemon'; import CardItem from './CardItem'; +import type { ChecklistV2, VariantKey } from '@/lib/checklist'; export default function CardGrid({ cards, - collected, + checklist, onToggle, }: { cards: TcgCard[]; - collected: Set; - onToggle: (id: string) => void; + checklist: ChecklistV2; + onToggle: (id: string, key: VariantKey) => void; }) { return (
@@ -18,7 +19,7 @@ export default function CardGrid({ ) )} diff --git a/src/components/CardItem.tsx b/src/components/CardItem.tsx index 4d736e3..8534f19 100644 --- a/src/components/CardItem.tsx +++ b/src/components/CardItem.tsx @@ -3,8 +3,11 @@ import Image from 'next/image'; import React from 'react'; import type { TcgCard } from '@/types/pokemon'; import SetBadge from './SetBadge'; +import type { VariantKey, VariantState } from '@/lib/checklist'; -export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; checked: boolean; onToggle: (id:string)=>void }) { +export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; checked: VariantState; onToggle: (id:string, key: VariantKey)=>void }) { + const hasHolo = !!(card.variants?.holofoil || (card.tcgplayer as any)?.prices?.holofoil); + const hasReverse = !!(card.variants?.reverseHolofoil || (card.tcgplayer as any)?.prices?.reverseHolofoil); return (
@@ -22,17 +25,41 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch {card.rarity ? {card.rarity} : null}
-
+
+ {hasHolo && ( + + )} + {hasReverse && ( + + )}
diff --git a/src/lib/checklist.ts b/src/lib/checklist.ts index 0a5188d..effdef7 100644 --- a/src/lib/checklist.ts +++ b/src/lib/checklist.ts @@ -1,9 +1,15 @@ -export const CHECKLIST_KEY = 'magikarp-checklist-v1'; +export const CHECKLIST_KEY_V1 = 'magikarp-checklist-v1'; +export const CHECKLIST_KEY_V2 = 'magikarp-checklist-v2'; -export function loadChecklist(): Set { +export type VariantKey = 'base' | 'holofoil' | 'reverseHolofoil'; +export type VariantState = Partial>; +export type ChecklistV2 = Record; // cardId -> variant flags + +// Legacy loader (v1) +export function loadChecklistV1(): Set { if (typeof window === 'undefined') return new Set(); try { - const raw = localStorage.getItem(CHECKLIST_KEY); + const raw = localStorage.getItem(CHECKLIST_KEY_V1); if (!raw) return new Set(); const arr = JSON.parse(raw) as string[]; return new Set(arr); @@ -12,10 +18,30 @@ export function loadChecklist(): Set { } } -export function saveChecklist(set: Set) { +// New storage (v2) +export function loadChecklist(): ChecklistV2 { + if (typeof window === 'undefined') return {}; + try { + const v2 = localStorage.getItem(CHECKLIST_KEY_V2); + if (v2) return JSON.parse(v2) as ChecklistV2; + // migrate from v1 if present + const v1 = loadChecklistV1(); + if (v1.size === 0) return {}; + const migrated: ChecklistV2 = {}; + v1.forEach((id) => { + migrated[id] = { base: true }; + }); + saveChecklist(migrated); + return migrated; + } catch { + return {}; + } +} + +export function saveChecklist(state: ChecklistV2) { if (typeof window === 'undefined') return; try { - localStorage.setItem(CHECKLIST_KEY, JSON.stringify(Array.from(set))); + localStorage.setItem(CHECKLIST_KEY_V2, JSON.stringify(state)); } catch { // ignore } diff --git a/src/types/pokemon.ts b/src/types/pokemon.ts index df90287..b1de7c6 100644 --- a/src/types/pokemon.ts +++ b/src/types/pokemon.ts @@ -20,6 +20,18 @@ export type TcgCard = { large: string; }; set: TcgSet; + // Optional variant availability flags, derived from API pricing/variant data + variants?: { + holofoil?: boolean; + reverseHolofoil?: boolean; + }; + // Subset of tcgplayer info used to infer variant availability + tcgplayer?: { + prices?: { + holofoil?: unknown; + reverseHolofoil?: unknown; + }; + }; }; export type CardsResponse = {