Files
bricktrack/src/app/w/[shareToken]/page.tsx
T
unknownandClaude Opus 5 c23b754515 feat(sammler): mehrere sammler mit eigener sammlung und wunschliste
Der Katalog bleibt gemeinsam - ein Set existiert einmal, unabhaengig
davon, wer es hat. Besitz und Wunschlisten gehoeren dagegen je einem
Sammler. Damit beantwortet "hat er das schon?" jetzt die Frage "hat *er*
das schon?": ein Set, das nur die Schwester besitzt, faellt fuer den
Bruder wieder unter "fehlt noch" und darf auf seine Wunschliste.

Wichtig fuer den Alltag: Solange es nur einen Sammler gibt, taucht die
Unterscheidung nirgends in der Oberflaeche auf. Kein Umschalter, keine
Abfrage beim Hinzufuegen, keine Badges - die App bleibt exakt so
einfach wie vorher. Erst der zweite Sammler schaltet all das frei.

Die Besitzlogik liegt weiterhin an genau einer Stelle
(src/lib/ownership.ts) und hat nur eine Dimension dazubekommen. Das
automatische "erfuellt" auf Wunschlisten fragt jetzt ebenfalls je Liste:
ein Wunsch auf Max' Liste ist erfuellt, wenn Max das Set bekommt - nicht
wenn Mia es hat. Listen ohne Sammler verhalten sich wie bisher.

Die Migration legt einen Sammler "Sammlung" an und ordnet ihm alles
Vorhandene zu; umbenennen laesst er sich unter Verwaltung -> Sammler.
Der Seed uebernimmt ihn statt einen dritten anzulegen.

Refs Phase 3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 09:00:35 +02:00

104 lines
3.6 KiB
TypeScript

import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { Blocks, CalendarDays, Gift } from 'lucide-react';
import { PublicWishlist, type PublicItem } from '@/components/wunschlisten/public-wishlist';
import { Badge } from '@/components/ui/badge';
import { formatDate, pluralize } from '@/lib/format';
import { occasionLabel } from '@/lib/labels';
import { getWishlistByShareToken, listWishlistItems } from '@/lib/queries/wishlists';
type Props = { params: Promise<{ shareToken: string }> };
/**
* The public face of a wishlist.
*
* Reachable with the share token alone, and it exposes nothing beyond the
* wishlist itself - no owner name, no collection, no other lists. Search
* engines are kept out; this is meant to be shared, not found.
*/
export const metadata: Metadata = {
title: 'Wunschliste',
robots: { index: false, follow: false },
};
export default async function PublicWishlistPage({ params }: Props) {
const { shareToken } = await params;
const list = getWishlistByShareToken(shareToken);
if (!list || !list.isPublic) notFound();
const rows = listWishlistItems(list.id, list.collectorId);
// Fulfilled wishes are gone from the public view: the present is already
// under the tree, no reason to let someone buy it twice.
const open = rows.filter((row) => row.fulfilledAt === null);
const items: PublicItem[] = open.map((row) => ({
id: row.id,
setNumber: row.setNumber,
setName: row.setName,
year: row.year,
pieceCount: row.pieceCount,
imageUrl: row.imageUrl,
imagePath: row.imagePath,
retailPriceCents: row.retailPriceCents,
manufacturerName: row.manufacturerName,
manufacturerWebsite: row.manufacturerWebsite,
themeName: row.themeName,
priority: row.priority,
note: row.note,
isReserved: row.reservedAt !== null,
reservedByName: row.reservedByName,
}));
const openCount = items.filter((item) => !item.isReserved).length;
return (
<div className="mx-auto min-h-dvh w-full max-w-2xl px-4 py-8">
<header className="space-y-3 border-b border-border pb-6">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Blocks className="size-4 text-primary" aria-hidden />
Bricktrack
</div>
<h1 className="text-2xl font-semibold tracking-tight">{list.title}</h1>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="gap-1">
<Gift className="size-3" aria-hidden />
{occasionLabel(list.occasion)}
</Badge>
{list.eventDate ? (
<Badge variant="outline" className="gap-1">
<CalendarDays className="size-3" aria-hidden />
{formatDate(list.eventDate)}
</Badge>
) : null}
<span className="text-sm text-muted-foreground">
{pluralize(openCount, 'Wunsch noch offen', 'Wünsche noch offen')}
</span>
</div>
{list.note ? (
<p className="rounded-lg bg-muted p-3 text-sm whitespace-pre-line">{list.note}</p>
) : null}
<p className="text-xs text-muted-foreground">
Klick auf „Das schenke ich“, damit niemand dasselbe doppelt kauft. Eine Anmeldung
brauchst du dafür nicht.
</p>
</header>
<main className="py-6">
{items.length === 0 ? (
<p className="rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
Auf dieser Liste steht gerade nichts.
</p>
) : (
<PublicWishlist shareToken={shareToken} items={items} />
)}
</main>
</div>
);
}