Kachelliste mit Suche ueber Setnummer, Name und EAN sowie Filtern nach Hersteller, Thema, Jahr und Besitzstatus. Besitz kommt aus genau einer Stelle: src/lib/ownership.ts definiert "hat er nicht" als Katalogeintrag ohne collection_item mit Status ungleich sold, als korrelierte Subquery, damit mehrere Exemplare die Liste nicht verdoppeln. Detailseite mit Metadaten, Anleitungen (PDF-Upload plus pdf.js-Viewer ohne CDN, oder externer Link), Fotos, Sammlungseintraegen und den Aktionen "Zur Sammlung" und "Auf Wunschliste". Upload-Pipeline prueft Magic Bytes statt des gemeldeten MIME-Typs, erzeugt Dateinamen serverseitig, wandelt Bilder in WebP inklusive Thumbnail und liefert Dateien ueber /api/files aus - Set-Bilder oeffentlich, Fotos und PDFs nur mit Session. Dazu das Seed-Skript mit 3 Herstellern, 6 Themen, 10 Sets und 4 Sammlungseintraegen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
55 lines
1.6 KiB
TypeScript
55 lines
1.6 KiB
TypeScript
'use client';
|
|
|
|
import { usePathname, useSearchParams } from 'next/navigation';
|
|
import Link from 'next/link';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
export function Pagination({
|
|
page,
|
|
total,
|
|
pageSize,
|
|
}: {
|
|
page: number;
|
|
total: number;
|
|
pageSize: number;
|
|
}) {
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
const lastPage = Math.max(1, Math.ceil(total / pageSize));
|
|
|
|
if (lastPage <= 1) return null;
|
|
|
|
function hrefFor(target: number) {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
if (target <= 1) params.delete('seite');
|
|
else params.set('seite', String(target));
|
|
const query = params.toString();
|
|
return query ? `${pathname}?${query}` : pathname;
|
|
}
|
|
|
|
return (
|
|
<nav className="flex items-center justify-between gap-3" aria-label="Seiten">
|
|
<Button asChild variant="outline" disabled={page <= 1}>
|
|
<Link href={hrefFor(page - 1)} aria-disabled={page <= 1} className={page <= 1 ? 'pointer-events-none opacity-50' : ''}>
|
|
<ChevronLeft />
|
|
Zurück
|
|
</Link>
|
|
</Button>
|
|
<span className="text-sm text-muted-foreground">
|
|
Seite {page} von {lastPage}
|
|
</span>
|
|
<Button asChild variant="outline" disabled={page >= lastPage}>
|
|
<Link
|
|
href={hrefFor(page + 1)}
|
|
aria-disabled={page >= lastPage}
|
|
className={page >= lastPage ? 'pointer-events-none opacity-50' : ''}
|
|
>
|
|
Weiter
|
|
<ChevronRight />
|
|
</Link>
|
|
</Button>
|
|
</nav>
|
|
);
|
|
}
|