feat(design): designauswahl mit klemmbaustein-look als standard

Sechs Designs (Klemmbaustein, Warm, Nachtblau, Bonbon, Bauklotz,
Schlicht) als Tokens je data-design, gewaehlt unter Konto und pro Geraet
im Cookie gespeichert. Dazu Noppen, Steinkontur, Platzhalter fuer Sets
ohne Bild, neues Logo, lokal gebuendelte Schriften und eine Startseite
mit Steinreihe und naechstem Termin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
unknown
2026-10-25 15:01:33 +01:00
co-authored by Claude Opus 5.5
parent e9093d0fad
commit 1b825b44c4
26 changed files with 1343 additions and 259 deletions
+53 -48
View File
@@ -1,6 +1,8 @@
import Link from 'next/link';
import { Check, Gift, ImageOff } from 'lucide-react';
import { Check, Gift } from 'lucide-react';
import { cn } from 'cn';
import { SetPlaceholder } from '@/components/brick/set-placeholder';
import { Studs } from '@/components/brick/studs';
import type { CatalogRow } from '@/lib/queries/catalog';
import { formatEuro } from '@/lib/money';
import { setImageSrc } from '@/lib/images';
@@ -17,56 +19,59 @@ export function SetCardContent({ set }: { set: CatalogRow }) {
const owned = set.ownedCopies > 0;
return (
<div className="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-card transition-colors">
<div className="relative aspect-square bg-muted">
{image ? (
// Plain img: images are either local files or a URL the user entered,
// and the optimizer must never fetch arbitrary remote hosts.
// eslint-disable-next-line @next/next/no-img-element
<img
src={image}
alt=""
loading="lazy"
decoding="async"
className="size-full object-contain p-2"
/>
) : (
<div className="flex size-full items-center justify-center text-muted-foreground">
<ImageOff className="size-8" aria-hidden />
<div className="relative h-full">
<Studs count={3} />
<div className="brick flex h-full flex-col overflow-hidden rounded-xl bg-card transition-colors">
<div className="relative aspect-square border-b-[length:var(--edge-w)] border-edge bg-muted">
{image ? (
// Plain img: images are either local files or a URL the user entered,
// and the optimizer must never fetch arbitrary remote hosts.
// eslint-disable-next-line @next/next/no-img-element
<img
src={image}
alt=""
loading="lazy"
decoding="async"
className="size-full object-contain p-2"
/>
) : (
<SetPlaceholder setNumber={set.setNumber} manufacturerName={set.manufacturerName} />
)}
<div className="absolute top-2 left-2 flex flex-col items-start gap-1">
{owned ? (
<Badge className="gap-1 bg-owned text-owned-foreground">
<Check className="size-3" aria-hidden />
{set.ownedCopies > 1 ? `${set.ownedCopies}× vorhanden` : 'Vorhanden'}
</Badge>
) : null}
{set.wishlistCount > 0 ? (
<Badge className="gap-1">
<Gift className="size-3" aria-hidden />
Auf Wunschliste
</Badge>
) : null}
</div>
)}
<div className="absolute top-2 left-2 flex flex-col items-start gap-1">
{owned ? (
<Badge className="gap-1 bg-emerald-600 text-white hover:bg-emerald-600">
<Check className="size-3" aria-hidden />
{set.ownedCopies > 1 ? `${set.ownedCopies}× vorhanden` : 'Vorhanden'}
</Badge>
) : null}
{set.wishlistCount > 0 ? (
<Badge variant="secondary" className="gap-1">
<Gift className="size-3" aria-hidden />
Auf Wunschliste
</Badge>
) : null}
</div>
</div>
<div className="flex flex-1 flex-col gap-1 p-3">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="font-mono">
{set.setNumber}
</Badge>
<span className="truncate">{set.manufacturerName}</span>
</div>
<p className="line-clamp-2 text-sm leading-snug font-medium">{set.name}</p>
<div className="mt-auto flex items-center justify-between gap-2 pt-1 text-xs text-muted-foreground">
<span className="truncate">{[set.themeName, set.year].filter(Boolean).join(' · ')}</span>
{set.retailPriceCents !== null ? (
<span className="shrink-0 font-medium text-foreground">
{formatEuro(set.retailPriceCents)}
<div className="flex flex-1 flex-col gap-1 p-3">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="font-mono">
{set.setNumber}
</Badge>
<span className="truncate">{set.manufacturerName}</span>
</div>
<p className="line-clamp-2 text-sm leading-snug font-medium">{set.name}</p>
<div className="mt-auto flex items-center justify-between gap-2 pt-1 text-xs text-muted-foreground">
<span className="truncate">
{[set.themeName, set.year].filter(Boolean).join(' · ')}
</span>
) : null}
{set.retailPriceCents !== null ? (
<span className="shrink-0 font-medium text-foreground">
{formatEuro(set.retailPriceCents)}
</span>
) : null}
</div>
</div>
</div>
</div>
@@ -78,7 +83,7 @@ export function SetCard({ set, className }: { set: CatalogRow; className?: strin
<Link
href={`/katalog/${set.id}`}
className={cn(
'rounded-xl outline-none focus-visible:ring-3 focus-visible:ring-ring/50 [&>div]:hover:border-primary/40',
'rounded-xl outline-none focus-visible:ring-3 focus-visible:ring-ring/50',
className,
)}
>