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>
This commit is contained in:
unknown
2026-09-07 09:00:35 +02:00
co-authored by Claude Opus 5
parent 7265c03548
commit c23b754515
35 changed files with 3104 additions and 66 deletions
+52 -4
View File
@@ -38,32 +38,47 @@ export function SetActions({
setName,
owned,
wishlists,
collectors,
}: {
setId: string;
setName: string;
owned: boolean;
wishlists: Array<{ id: string; title: string }>;
/** More than one turns "Zur Sammlung" into a question of "for whom". */
collectors: Array<{ id: string; name: string }>;
}) {
const router = useRouter();
const [pending, startTransition] = useTransition();
const [wishlistOpen, setWishlistOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [targetId, setTargetId] = useState(wishlists[0]?.id ?? NEW_LIST);
const [collectorOpen, setCollectorOpen] = useState(false);
const [collectorId, setCollectorId] = useState(collectors[0]?.id ?? '');
const [newTitle, setNewTitle] = useState('');
const [priority, setPriority] = useState('2');
function addToCollection() {
function addToCollection(forCollectorId?: string) {
startTransition(async () => {
const result = await addSetToCollection(setId);
const result = await addSetToCollection(setId, forCollectorId);
if (!result.ok) {
toast.error(result.error);
return;
}
toast.success('Zur Sammlung hinzugefügt.');
const owner = collectors.find((entry) => entry.id === forCollectorId);
toast.success(
owner ? `Zur Sammlung von ${owner.name} hinzugefügt.` : 'Zur Sammlung hinzugefügt.',
);
setCollectorOpen(false);
router.refresh();
});
}
// With a single collector there is nothing to ask.
function onAddClick() {
if (collectors.length > 1) setCollectorOpen(true);
else addToCollection();
}
function addToWishlist() {
startTransition(async () => {
let wishlistId = targetId;
@@ -111,7 +126,7 @@ export function SetActions({
return (
<div className="flex flex-wrap gap-2">
<Button size="lg" onClick={addToCollection} disabled={pending}>
<Button size="lg" onClick={onAddClick} disabled={pending}>
{pending ? <Loader2 className="animate-spin" /> : <Plus />}
Zur Sammlung hinzufügen
</Button>
@@ -138,6 +153,39 @@ export function SetActions({
</p>
) : null}
<Dialog open={collectorOpen} onOpenChange={setCollectorOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Wer bekommt das Set?</DialogTitle>
<DialogDescription>{setName}</DialogDescription>
</DialogHeader>
<Field label="Sammler" htmlFor="add-collector" required>
<NativeSelect
id="add-collector"
value={collectorId}
onChange={(event) => setCollectorId(event.target.value)}
>
{collectors.map((entry) => (
<option key={entry.id} value={entry.id}>
{entry.name}
</option>
))}
</NativeSelect>
</Field>
<DialogFooter>
<Button variant="outline" onClick={() => setCollectorOpen(false)} disabled={pending}>
Abbrechen
</Button>
<Button onClick={() => addToCollection(collectorId)} disabled={pending}>
{pending ? <Loader2 className="animate-spin" /> : <Plus />}
Hinzufügen
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={wishlistOpen} onOpenChange={setWishlistOpen}>
<DialogContent>
<DialogHeader>