feat(katalog): set-katalog mit filtern, detailseite und uploads
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>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user