Init commit from initial prompt
This commit is contained in:
25
src/app/api/magikarp/route.ts
Normal file
25
src/app/api/magikarp/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { fetchMagikarpCards } from '@/lib/api';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const q = searchParams.get('q') ?? undefined;
|
||||
const page = Number(searchParams.get('page') ?? '1');
|
||||
const pageSize = Number(searchParams.get('pageSize') ?? '250');
|
||||
const orderBy = searchParams.get('orderBy') ?? 'set.releaseDate';
|
||||
|
||||
try {
|
||||
const data = await fetchMagikarpCards({ q, page, pageSize });
|
||||
return Response.json(data, { status: 200 });
|
||||
} catch (err: any) {
|
||||
const hasKey = !!(process.env.POKEMON_TCG_API_KEY || process.env.NEXT_PUBLIC_POKEMON_TCG_API_KEY);
|
||||
console.error('Error fetching cards', err);
|
||||
if (!hasKey) {
|
||||
return Response.json(
|
||||
{ data: [], note: 'No API key configured. Create .env.local with NEXT_PUBLIC_POKEMON_TCG_API_KEY="<your key>"' },
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
return Response.json({ error: 'Failed to fetch cards' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
15
src/app/globals.css
Normal file
15
src/app/globals.css
Normal file
@@ -0,0 +1,15 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--bg: 248 250 252;
|
||||
--fg: 15 23 42;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Utility tweaks */
|
||||
.container {
|
||||
max-width: 80rem;
|
||||
}
|
||||
19
src/app/layout.tsx
Normal file
19
src/app/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Magikarp Collection',
|
||||
description: 'Checklist and gallery for all English Magikarp Pokémon cards',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className="h-full bg-slate-50">
|
||||
<body className="min-h-full text-slate-800">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
106
src/app/page.tsx
Normal file
106
src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user