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:
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useTransition } from 'react';
|
||||
import { cn } from 'cn';
|
||||
import { collectorDotClass } from '@/lib/collector-colors';
|
||||
|
||||
export type CollectorOption = { id: string; name: string; color: string };
|
||||
|
||||
/**
|
||||
* Switches the view between collectors.
|
||||
*
|
||||
* Rendered only when there is more than one - with a single collector the
|
||||
* whole question does not exist, and the app should not ask it. The choice
|
||||
* lives in the URL (`?sammler=`), so a filtered view stays shareable and the
|
||||
* back button behaves.
|
||||
*/
|
||||
export function CollectorFilter({
|
||||
collectors,
|
||||
allLabel = 'Alle',
|
||||
}: {
|
||||
collectors: CollectorOption[];
|
||||
allLabel?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
if (collectors.length < 2) return null;
|
||||
|
||||
const current = searchParams.get('sammler') ?? '';
|
||||
|
||||
function select(id: string) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (id) params.set('sammler', id);
|
||||
else params.delete('sammler');
|
||||
params.delete('seite');
|
||||
|
||||
const query = params.toString();
|
||||
startTransition(() => router.push(query ? `${pathname}?${query}` : pathname));
|
||||
}
|
||||
|
||||
const options = [{ id: '', name: allLabel, color: 'slate' }, ...collectors];
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Sammler"
|
||||
className="flex gap-1 overflow-x-auto rounded-lg bg-muted p-1"
|
||||
>
|
||||
{options.map((option) => {
|
||||
const active = current === option.id;
|
||||
return (
|
||||
<button
|
||||
key={option.id || 'alle'}
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
onClick={() => select(option.id)}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium whitespace-nowrap transition-colors',
|
||||
active
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{option.id ? (
|
||||
<span
|
||||
className={cn('size-2 rounded-full', collectorDotClass(option.color))}
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
{option.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user