Init commit from initial prompt

This commit is contained in:
2025-08-17 12:21:15 +01:00
commit caee8e27f2
21 changed files with 3114 additions and 0 deletions

106
src/app/page.tsx Normal file
View File

@@ -0,0 +1,106 @@
"use client";
import React, { useEffect, useMemo, useState } from 'react';
import type { TcgCard } from '@/types/pokemon';
import { loadChecklist, saveChecklist } 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<TcgCard[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [query, setQuery] = useState('');
const [collected, setCollected] = useState<Set<string>>(new Set());
const [setId, setSetId] = useState('');
const [note, setNote] = useState<string | null>(null);
useEffect(() => {
setCollected(loadChecklist());
}, []);
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();
setCards(json.data || []);
if (json.note) setNote(String(json.note));
} catch (e: any) {
setError(e?.message || 'Error');
} finally {
setLoading(false);
}
}
load();
}, []);
function toggleCollected(id: string) {
setCollected((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id); else next.add(id);
saveChecklist(next);
return next;
});
}
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]);
const setOptions = useMemo(() => {
const map = new Map<string, string>();
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]);
return (
<main className="container mx-auto">
<Header
query={query}
onQueryChange={setQuery}
total={cards.length}
collectedCount={collected.size}
/>
<div className="mb-4 flex flex-wrap items-center gap-3">
<SetFilter options={setOptions} value={setId} onChange={setSetId} />
</div>
{loading ? (
<p className="text-slate-600">Loading Magikarp cards</p>
) : error ? (
<p className="text-red-600">{error}</p>
) : (
<>
{note ? (
<div className="mb-4 rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-800 text-sm">
{note}
</div>
) : null}
<div className="mb-4 text-sm text-slate-600">
Showing {filtered.length} of {cards.length} cards.
</div>
<CardGrid cards={filtered} collected={collected} onToggle={toggleCollected} />
</>
)}
</main>
);
}