diff --git a/src/app/api/magikarp/route.ts b/src/app/api/magikarp/route.ts index f240791..9cf3723 100644 --- a/src/app/api/magikarp/route.ts +++ b/src/app/api/magikarp/route.ts @@ -12,6 +12,14 @@ async function ensureDataDir() { try { await fs.mkdir(DATA_DIR, { recursive: true }); } catch {} } +function applyReverseOverrides(payload: any, reverseMap: Record): any { + if (!payload) return payload; + if (Array.isArray(payload?.data)) { + return { ...payload, data: payload.data.map((c: any) => (reverseMap[c.id] ? { ...c, variants: { ...(c.variants || {}), reverseHolofoil: true } } : c)) }; + } + return payload; +} + export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const q = searchParams.get('q') ?? undefined; @@ -34,16 +42,7 @@ export async function GET(req: NextRequest) { try { const raw = await fs.readFile(CARDS_FILE, 'utf-8'); if (raw) { - const json = JSON.parse(raw); - // Apply overrides on the fly to cached payload - if (Array.isArray(json?.data)) { - json.data = json.data.map((c: any) => { - if (reverseMap[c.id]) { - c.variants = { ...(c.variants || {}), reverseHolofoil: true }; - } - return c; - }); - } + const json = applyReverseOverrides(JSON.parse(raw), reverseMap); const ts = json?.updatedAt ? Date.parse(json.updatedAt) : NaN; const fresh = Number.isFinite(ts) && (Date.now() - ts) < TTL_MS; if (fresh) { @@ -56,9 +55,7 @@ export async function GET(req: NextRequest) { // Fetch fresh from API const data = await fetchMagikarpCards({ q, page, pageSize }); // Apply overrides to fresh data - const patched = Array.isArray(data?.data) - ? { ...data, data: data.data.map((c: any) => (reverseMap[c.id] ? { ...c, variants: { ...(c.variants || {}), reverseHolofoil: true } } : c)) } - : data; + const patched = applyReverseOverrides(data, reverseMap); const payload = { ...patched, cached: false, updatedAt: new Date().toISOString() }; // Write cache best-effort try { await fs.writeFile(CARDS_FILE, JSON.stringify(payload, null, 2), 'utf-8'); } catch {} diff --git a/src/app/page.tsx b/src/app/page.tsx index 90f1ff3..e0f0ecb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,8 @@ import type { ChecklistV2, VariantKey } from '@/lib/checklist'; import CardGrid from '@/components/CardGrid'; import Header from '@/components/Header'; import SetFilter from '@/components/SetFilter'; +import { sortCards, detectVariants } from '@/lib/cards'; +import { loadOverridesClient, REVERSE_OVERRIDE_EVENT } from '@/lib/overrides'; export default function Page() { const [cards, setCards] = useState([]); @@ -32,13 +34,8 @@ export default function Page() { // 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 {} + const map = await loadOverridesClient(); + if (map && typeof map === 'object') setOverrides(map); })(); // Listen for UI-initiated override changes function onOverrideChanged(ev: Event) { @@ -49,8 +46,8 @@ export default function Page() { setOverrides((prev) => ({ ...prev, [id]: !!reverse })); } catch {} } - window.addEventListener('reverse-override-changed', onOverrideChanged as EventListener); - return () => window.removeEventListener('reverse-override-changed', onOverrideChanged as EventListener); + window.addEventListener(REVERSE_OVERRIDE_EVENT, onOverrideChanged as EventListener); + return () => window.removeEventListener(REVERSE_OVERRIDE_EVENT, onOverrideChanged as EventListener); }, []); useEffect(() => { @@ -61,23 +58,7 @@ export default function Page() { 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); - }); + const list: TcgCard[] = (json.data || []).slice().sort(sortCards); setCards(list); if (json.note) setNote(String(json.note)); if (json.updatedAt) setUpdatedAt(String(json.updatedAt)); @@ -98,22 +79,7 @@ export default function Page() { const res = await fetch('/api/magikarp?pageSize=250&refresh=1', { cache: 'no-store' }); if (!res.ok) throw new Error(`Failed to refresh: ${res.body}`); 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); - }); + const list: TcgCard[] = (json.data || []).slice().sort(sortCards); setCards(list); if (json.note) setNote(String(json.note)); else setNote(null); if (json.updatedAt) setUpdatedAt(String(json.updatedAt)); @@ -144,12 +110,7 @@ export default function Page() { } 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; + return detectVariants(card, overrides); } function isFullyCollected(card: TcgCard): boolean { diff --git a/src/components/CardItem.tsx b/src/components/CardItem.tsx index 85ff0a7..61cafd2 100644 --- a/src/components/CardItem.tsx +++ b/src/components/CardItem.tsx @@ -5,6 +5,7 @@ import type { TcgCard } from '@/types/pokemon'; import SetBadge from './SetBadge'; import type { VariantKey, VariantState } from '@/lib/checklist'; import { useUsdGbpRate } from '@/lib/useExchangeRate'; +import { dispatchReverseOverride, loadOverridesClient } from '@/lib/overrides'; export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; checked: VariantState; onToggle: (id:string, key: VariantKey)=>void }) { const rate = useUsdGbpRate(); @@ -42,19 +43,12 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch React.useEffect(() => { let cancelled = false; (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 || {}; - if (!cancelled && map && typeof map === 'object') { - if (map[card.id]) { - setReverseOverride(true); - setReverseRemoved(false); - } + const map = await loadOverridesClient(); + if (!cancelled && map && typeof map === 'object') { + if (map[card.id]) { + setReverseOverride(true); + setReverseRemoved(false); } - } catch { - // ignore } })(); return () => { cancelled = true; }; @@ -118,7 +112,7 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch }); setReverseOverride(true); // Notify app to recompute availableVariants immediately - try { window.dispatchEvent(new CustomEvent('reverse-override-changed', { detail: { id: card.id, reverse: true } })); } catch {} + dispatchReverseOverride(card.id, true); } catch {} }} > @@ -142,7 +136,7 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch setReverseOverride(false); setReverseRemoved(true); // Notify app to recompute availableVariants immediately - try { window.dispatchEvent(new CustomEvent('reverse-override-changed', { detail: { id: card.id, reverse: false } })); } catch {} + dispatchReverseOverride(card.id, false); } catch {} }} > diff --git a/src/lib/cards.ts b/src/lib/cards.ts new file mode 100644 index 0000000..bef6358 --- /dev/null +++ b/src/lib/cards.ts @@ -0,0 +1,28 @@ +import type { TcgCard } from '@/types/pokemon'; +import type { VariantKey } from '@/lib/checklist'; + +export function sortCards(a: TcgCard, b: TcgCard): number { + 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); +} + +export function detectVariants(card: TcgCard, overrides?: Record): 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; +} diff --git a/src/lib/overrides.ts b/src/lib/overrides.ts new file mode 100644 index 0000000..b862526 --- /dev/null +++ b/src/lib/overrides.ts @@ -0,0 +1,19 @@ +export type OverridesMap = Record; + +export const REVERSE_OVERRIDE_EVENT = 'reverse-override-changed'; + +export async function loadOverridesClient(): Promise { + 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 OverridesMap; + return map && typeof map === 'object' ? map : {}; + } catch { + return {}; + } +} + +export function dispatchReverseOverride(id: string, reverse: boolean): void { + try { window.dispatchEvent(new CustomEvent(REVERSE_OVERRIDE_EVENT, { detail: { id, reverse } })); } catch {} +}