From d5ffcbae6049a5d1e5da8c1aa6432974b30895f6 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 25 Oct 2026 18:50:13 +0100 Subject: [PATCH 1/2] feat(navigation): plus in der mitte, mehr-seite und sammler-umschalter Unten Start, Katalog, Sammlung, Wuensche und ein Plus mit allen Wegen zu einem neuen Set. Konto, Design und Stammdaten auf der Seite "Mehr" statt im Konto-Menue und in sieben Reitern. Bei mehreren Sammlern waehlt "Fuer wen?" in der Kopfzeile das Kind fuer die ganze App, gemerkt pro Geraet. Behebt nebenbei, dass Mitglieder die Stammdaten nicht erreichten. Co-Authored-By: Claude Opus 5.5 --- e2e/screenshots.spec.ts | 18 ++- src/app/(app)/collector-actions.ts | 38 +++++ src/app/(app)/katalog/[id]/page.tsx | 5 +- src/app/(app)/katalog/neu/page.tsx | 11 +- src/app/(app)/katalog/page.tsx | 11 +- src/app/(app)/layout.tsx | 20 ++- src/app/(app)/mehr/page.tsx | 66 ++++++++ src/app/(app)/page.tsx | 30 ++-- src/app/(app)/sammlung/page.tsx | 11 +- src/app/(app)/verwaltung/layout.tsx | 12 +- src/components/katalog/barcode-scanner.tsx | 116 +++++++------- src/components/katalog/link-import.tsx | 13 +- src/components/katalog/rebrickable-import.tsx | 3 +- src/components/layout/add-menu.tsx | 142 ++++++++++++++++++ src/components/layout/app-header.tsx | 42 +++++- src/components/layout/bottom-nav.tsx | 93 +++++++----- src/components/layout/collector-filter.tsx | 78 ---------- src/components/layout/collector-switch.tsx | 90 +++++++++++ src/components/layout/more-items.ts | 115 ++++++++++++++ src/components/layout/sign-out-button.tsx | 29 ++++ src/components/layout/user-menu.tsx | 65 -------- .../verwaltung/verwaltung-header.tsx | 28 ++++ src/components/verwaltung/verwaltung-nav.tsx | 43 ------ src/lib/collector-choice.ts | 24 +++ 24 files changed, 780 insertions(+), 323 deletions(-) create mode 100644 src/app/(app)/collector-actions.ts create mode 100644 src/app/(app)/mehr/page.tsx create mode 100644 src/components/layout/add-menu.tsx delete mode 100644 src/components/layout/collector-filter.tsx create mode 100644 src/components/layout/collector-switch.tsx create mode 100644 src/components/layout/more-items.ts create mode 100644 src/components/layout/sign-out-button.tsx delete mode 100644 src/components/layout/user-menu.tsx create mode 100644 src/components/verwaltung/verwaltung-header.tsx delete mode 100644 src/components/verwaltung/verwaltung-nav.tsx create mode 100644 src/lib/collector-choice.ts diff --git a/e2e/screenshots.spec.ts b/e2e/screenshots.spec.ts index 93f99be..62ed79c 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -52,6 +52,17 @@ test('Screenshots fuer die Anleitung', async ({ page, browser }) => { await visit(page, '/'); await shoot(page, '02-start'); + // The plus in the middle of the navigation, opened. + await page.getByRole('button', { name: 'Neues Set' }).click(); + await expect(page.getByText('Was kommt dazu?')).toBeVisible(); + await page.waitForTimeout(400); // let the sheet finish sliding in + await page.screenshot({ path: shotPath('26-plus-menue') }); + await page.keyboard.press('Escape'); + + await visit(page, '/mehr'); + await expect(page.getByText('Katalog pflegen')).toBeVisible(); + await shoot(page, '27-mehr'); + // --- 3. master data ------------------------------------------------------- await visit(page, '/verwaltung/hersteller'); await expect(page.getByText('Lego', { exact: true })).toBeVisible(); @@ -186,10 +197,13 @@ test('Screenshots fuer die Anleitung', async ({ page, browser }) => { // The switcher only exists because the seed created two collectors. await visit(page, '/katalog?besitz=fehlt'); - await expect(page.getByRole('button', { name: /Max/ })).toBeVisible(); - await page.getByRole('button', { name: /Max/ }).click(); + await expect(page.getByRole('radio', { name: /Max/ })).toBeVisible(); + await page.getByRole('radio', { name: /Max/ }).click(); await expect(page.getByText('Sets, die Max noch nicht hat')).toBeVisible(); await shoot(page, '22-sammler-filter'); + // Back to everybody, so the following pictures show the whole family. + await page.getByRole('radio', { name: /Alle/ }).click(); + await expect(page.getByText('Sets, die noch nicht in der Sammlung sind')).toBeVisible(); // --- 17. prices ----------------------------------------------------------- await visit(page, '/katalog?q=60398'); diff --git a/src/app/(app)/collector-actions.ts b/src/app/(app)/collector-actions.ts new file mode 100644 index 0000000..24e73fc --- /dev/null +++ b/src/app/(app)/collector-actions.ts @@ -0,0 +1,38 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { cookies } from 'next/headers'; +import { z } from 'zod'; +import { env } from '@/env'; +import { actionError, actionOk, type ActionResult } from '@/lib/action-result'; +import { COLLECTOR_COOKIE } from '@/lib/collector-choice'; +import { resolveCollectorId } from '@/lib/queries/collectors'; +import { requireUser } from '@/lib/session'; + +const schema = z.object({ id: z.string().max(64) }); + +/** Remembers the collector chosen in the header; an empty id means "alle". */ +export async function chooseCollectorAction(input: unknown): Promise { + await requireUser(); + + const parsed = schema.safeParse(input); + if (!parsed.success) return actionError('Ungültige Auswahl.'); + + const store = await cookies(); + if (parsed.data.id === '') { + store.delete(COLLECTOR_COOKIE); + } else { + const id = resolveCollectorId(parsed.data.id); + if (!id) return actionError('Diesen Sammler gibt es nicht mehr.'); + store.set(COLLECTOR_COOKIE, id, { + httpOnly: true, + secure: env.isProduction, + sameSite: 'lax', + path: '/', + maxAge: 60 * 60 * 24 * 365, + }); + } + + revalidatePath('/', 'layout'); + return actionOk(); +} diff --git a/src/app/(app)/katalog/[id]/page.tsx b/src/app/(app)/katalog/[id]/page.tsx index 527ec1d..0d83de0 100644 --- a/src/app/(app)/katalog/[id]/page.tsx +++ b/src/app/(app)/katalog/[id]/page.tsx @@ -23,7 +23,8 @@ import { listPricePoints, } from '@/lib/queries/catalog'; import { listMissingParts } from '@/lib/queries/collection'; -import { listCollectors, resolveCollectorId } from '@/lib/queries/collectors'; +import { activeCollectorId } from '@/lib/collector-choice'; +import { listCollectors } from '@/lib/queries/collectors'; import { listLocations } from '@/lib/queries/master-data'; import { listWishlistsForSet, listWishlistTargets } from '@/lib/queries/wishlists'; import { requireUser } from '@/lib/session'; @@ -47,7 +48,7 @@ export default async function SetDetailPage({ params, searchParams }: Props) { // Keeps the "vorhanden / fehlt" badge in step with the collector the user // was looking at in the catalog. const rawCollector = Array.isArray(query.sammler) ? query.sammler[0] : query.sammler; - const collectorId = resolveCollectorId(rawCollector); + const collectorId = await activeCollectorId(rawCollector); const collectors = listCollectors(); const set = getSetById(id, collectorId); diff --git a/src/app/(app)/katalog/neu/page.tsx b/src/app/(app)/katalog/neu/page.tsx index e19c47d..2d76de3 100644 --- a/src/app/(app)/katalog/neu/page.tsx +++ b/src/app/(app)/katalog/neu/page.tsx @@ -15,6 +15,8 @@ export default async function NeuesSetPage({ searchParams }: { searchParams: Sea await requireUser(); const params = await searchParams; const ean = typeof params.ean === 'string' ? params.ean : undefined; + // Which way the plus menu chose: the matching field gets the cursor. + const way = typeof params.weg === 'string' ? params.weg : undefined; const manufacturers = listManufacturers().map((row) => ({ id: row.id, name: row.name })); const themes = listThemes().map((row) => ({ @@ -38,9 +40,14 @@ export default async function NeuesSetPage({ searchParams }: { searchParams: Sea {/* The Lego import creates its manufacturer itself, so it works even on a completely empty instance. */} - + - +

Für ein Set von Hand brauchst du zuerst einen Hersteller – Lego, Playmobil, Cobi oder was auch immer im Kinderzimmer steht. diff --git a/src/app/(app)/katalog/page.tsx b/src/app/(app)/katalog/page.tsx index e76a3d0..f23775a 100644 --- a/src/app/(app)/katalog/page.tsx +++ b/src/app/(app)/katalog/page.tsx @@ -1,7 +1,6 @@ import type { Metadata } from 'next'; import Link from 'next/link'; import { Plus } from 'lucide-react'; -import { CollectorFilter } from '@/components/layout/collector-filter'; import { BarcodeScannerButton } from '@/components/katalog/barcode-scanner'; import { CatalogFilters } from '@/components/katalog/catalog-filters'; import { CatalogGrid } from '@/components/katalog/catalog-grid'; @@ -10,7 +9,8 @@ import { Button } from '@/components/ui/button'; import { pluralize } from '@/lib/format'; import { OWNERSHIP_FILTER_LABELS, parseOwnershipFilter } from '@/lib/ownership'; import { CATALOG_PAGE_SIZE, listSets, listSetYears } from '@/lib/queries/catalog'; -import { listCollectors, resolveCollectorId } from '@/lib/queries/collectors'; +import { activeCollectorId } from '@/lib/collector-choice'; +import { listCollectors } from '@/lib/queries/collectors'; import { listManufacturers, listThemes } from '@/lib/queries/master-data'; import { listWishlistTargets } from '@/lib/queries/wishlists'; import { requireUser } from '@/lib/session'; @@ -30,7 +30,7 @@ export default async function KatalogPage({ searchParams }: { searchParams: Sear const ownership = parseOwnershipFilter(single(params.besitz)); const collectors = listCollectors(); - const collectorId = resolveCollectorId(single(params.sammler)); + const collectorId = await activeCollectorId(single(params.sammler)); const activeCollector = collectors.find((row) => row.id === collectorId) ?? null; const yearParam = single(params.jahr); const page = Number(single(params.seite) ?? '1') || 1; @@ -80,11 +80,6 @@ export default async function KatalogPage({ searchParams }: { searchParams: Sear - ({ id: row.id, name: row.name, color: row.color }))} - allLabel="Alle zusammen" - /> -

diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 69963e7..678423d 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -1,5 +1,8 @@ import { AppHeader } from '@/components/layout/app-header'; import { BottomNav } from '@/components/layout/bottom-nav'; +import { storedCollectorId } from '@/lib/collector-choice'; +import { listCollectors } from '@/lib/queries/collectors'; +import { isRebrickableEnabled } from '@/lib/rebrickable'; import { ensureReminderScheduler } from '@/lib/reminder-job'; import { requireUser } from '@/lib/session'; @@ -9,13 +12,24 @@ export default async function AppLayout({ children }: { children: React.ReactNod ensureReminderScheduler(); const user = await requireUser(); + const collectors = listCollectors().map((row) => ({ + id: row.id, + name: row.name, + color: row.color, + })); + const legoImport = isRebrickableEnabled(); return (

- + {/* Bottom padding keeps the mobile nav from covering the last row. */} -
{children}
- +
{children}
+
); } diff --git a/src/app/(app)/mehr/page.tsx b/src/app/(app)/mehr/page.tsx new file mode 100644 index 0000000..0916a10 --- /dev/null +++ b/src/app/(app)/mehr/page.tsx @@ -0,0 +1,66 @@ +import type { Metadata } from 'next'; +import Link from 'next/link'; +import { cn } from 'cn'; +import { Studs } from '@/components/brick/studs'; +import { MORE_GROUPS, moreItemsFor } from '@/components/layout/more-items'; +import { SignOutButton } from '@/components/layout/sign-out-button'; +import { requireUser } from '@/lib/session'; + +export const metadata: Metadata = { title: 'Mehr' }; + +const GROUP_TONES: Record = { + ich: 'bg-tone-1', + familie: 'bg-tone-2', + katalog: 'bg-tone-3', + daten: 'bg-tone-4', +}; + +export default async function MehrPage() { + const user = await requireUser(); + const items = moreItemsFor(user.role); + + return ( +
+
+
+

{user.name}

+

+ {user.email} · {user.role === 'admin' ? 'Administrator' : 'Mitglied'} +

+
+
+ + {MORE_GROUPS.map((group) => { + const entries = items.filter((item) => item.group === group.id); + if (entries.length === 0) return null; + return ( +
+

+ {group.label} +

+
    + {entries.map((item) => ( +
  • + + + + {item.label} + {item.description} + +
  • + ))} +
+
+ ); + })} + + +
+ ); +} diff --git a/src/app/(app)/page.tsx b/src/app/(app)/page.tsx index 60b2f5f..36253f9 100644 --- a/src/app/(app)/page.tsx +++ b/src/app/(app)/page.tsx @@ -4,6 +4,7 @@ import { cn } from 'cn'; import { Studs } from '@/components/brick/studs'; import { Button } from '@/components/ui/button'; import { brickRow } from '@/lib/brick-row'; +import { activeCollectorId } from '@/lib/collector-choice'; import { collectorDotClass } from '@/lib/collector-colors'; import { formatDate, formatNumber, pluralize } from '@/lib/format'; import { formatEuro } from '@/lib/money'; @@ -19,22 +20,31 @@ function daysLabel(days: number): string { return `in ${days} Tagen`; } -export default async function DashboardPage() { - const user = await requireUser(); +type SearchParams = Promise>; - const stats = getCollectionStats(); - const missing = countMissingSets(); +export default async function DashboardPage({ searchParams }: { searchParams: SearchParams }) { + const user = await requireUser(); + const params = await searchParams; + const rawCollector = Array.isArray(params.sammler) ? params.sammler[0] : params.sammler; + + // With a child chosen in the header, the start page is about that child. + const collectorId = await activeCollectorId(rawCollector); + const stats = getCollectionStats(collectorId); + const missing = countMissingSets(collectorId); const totalSets = stats.setCount + missing; const collectors = listCollectors(); + const activeCollector = collectors.find((entry) => entry.id === collectorId) ?? null; const perCollector = countItemsPerCollector(); - const showCollectors = collectors.length > 1; + const showCollectors = collectors.length > 1 && !activeCollector; const bricks = brickRow( stats.setCount, totalSets, - showCollectors - ? collectors.map((entry) => ({ key: entry.id, count: perCollector.get(entry.id) ?? 0 })) - : [], + activeCollector + ? [{ key: activeCollector.id, count: 1 }] + : showCollectors + ? collectors.map((entry) => ({ key: entry.id, count: perCollector.get(entry.id) ?? 0 })) + : [], ); const colorById = new Map(collectors.map((entry) => [entry.id, entry.color])); @@ -53,7 +63,9 @@ export default async function DashboardPage() {
-

Unsere Sammlung

+

+ {activeCollector ? `Sammlung von ${activeCollector.name}` : 'Unsere Sammlung'} +

{formatNumber(stats.setCount)} von {pluralize(totalSets, 'Set', 'Sets')}

diff --git a/src/app/(app)/sammlung/page.tsx b/src/app/(app)/sammlung/page.tsx index b101959..3dff3d2 100644 --- a/src/app/(app)/sammlung/page.tsx +++ b/src/app/(app)/sammlung/page.tsx @@ -3,7 +3,6 @@ import Link from 'next/link'; import { Boxes, Euro, Layers, PackageSearch } from 'lucide-react'; import { SetPlaceholder } from '@/components/brick/set-placeholder'; import { Studs } from '@/components/brick/studs'; -import { CollectorFilter } from '@/components/layout/collector-filter'; import { CollectionFilters } from '@/components/sammlung/collection-filters'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent } from '@/components/ui/card'; @@ -19,7 +18,8 @@ import { listCollectionItems, type CollectionFilters as Filters, } from '@/lib/queries/collection'; -import { listCollectors, resolveCollectorId } from '@/lib/queries/collectors'; +import { activeCollectorId } from '@/lib/collector-choice'; +import { listCollectors } from '@/lib/queries/collectors'; import { listLocations, listManufacturers } from '@/lib/queries/master-data'; import { requireUser } from '@/lib/session'; import type { CollectionStatus, Completeness } from '@/db/types'; @@ -38,7 +38,7 @@ export default async function SammlungPage({ searchParams }: { searchParams: Sea const params = await searchParams; const collectors = listCollectors(); - const collectorId = resolveCollectorId(single(params.sammler)); + const collectorId = await activeCollectorId(single(params.sammler)); const activeCollector = collectors.find((row) => row.id === collectorId) ?? null; const filters: Filters = { @@ -78,11 +78,6 @@ export default async function SammlungPage({ searchParams }: { searchParams: Sea

- ({ id: row.id, name: row.name, color: row.color }))} - allLabel="Alle zusammen" - /> -
{tiles.map((tile) => ( diff --git a/src/app/(app)/verwaltung/layout.tsx b/src/app/(app)/verwaltung/layout.tsx index ae01e31..616a6ba 100644 --- a/src/app/(app)/verwaltung/layout.tsx +++ b/src/app/(app)/verwaltung/layout.tsx @@ -1,21 +1,15 @@ import type { Metadata } from 'next'; -import { VerwaltungNav } from '@/components/verwaltung/verwaltung-nav'; +import { VerwaltungHeader } from '@/components/verwaltung/verwaltung-header'; import { requireUser } from '@/lib/session'; export const metadata: Metadata = { title: 'Verwaltung' }; export default async function VerwaltungLayout({ children }: { children: React.ReactNode }) { - const user = await requireUser(); + await requireUser(); return (
-
-

Verwaltung

-

- Stammdaten, die im Katalog und in der Sammlung ausgewählt werden. -

-
- + {children}
); diff --git a/src/components/katalog/barcode-scanner.tsx b/src/components/katalog/barcode-scanner.tsx index f5fa78f..9bbf54a 100644 --- a/src/components/katalog/barcode-scanner.tsx +++ b/src/components/katalog/barcode-scanner.tsx @@ -25,13 +25,18 @@ type Controls = { stop: () => void }; * loaded only when the scanner is opened - nothing leaves the device, and the * catalog pages stay light for everyone who never scans. */ -export function BarcodeScannerButton() { +export function BarcodeScannerDialog({ + open, + onOpenChange: setOpen, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { const router = useRouter(); const videoRef = useRef(null); const controlsRef = useRef(null); const handledRef = useRef(false); - const [open, setOpen] = useState(false); const [status, setStatus] = useState<'starting' | 'scanning' | 'checking' | 'error'>('starting'); const [message, setMessage] = useState(null); @@ -68,7 +73,7 @@ export function BarcodeScannerButton() { router.refresh(); }); }, - [router, stop], + [router, setOpen, stop], ); useEffect(() => { @@ -134,6 +139,61 @@ export function BarcodeScannerButton() { }; }, [open, onDetected, stop]); + return ( + { + if (!next) stop(); + setOpen(next); + }} + > + + + Barcode scannen + + Halte den Strichcode auf der Packung ins Bild. Kennt der Katalog das Set schon, + landest du direkt darauf – sonst im Formular mit ausgefüllter EAN. + + + + {status === 'error' ? ( +

+ {message} +

+ ) : ( +
+
+ ); +} + +export function BarcodeScannerButton() { + const [open, setOpen] = useState(false); + return ( <> - - { - if (!next) stop(); - setOpen(next); - }} - > - - - Barcode scannen - - Halte den Strichcode auf der Packung ins Bild. Kennt der Katalog das Set schon, - landest du direkt darauf – sonst im Formular mit ausgefüllter EAN. - - - - {status === 'error' ? ( -

- {message} -

- ) : ( -
-
+ ); } diff --git a/src/components/katalog/link-import.tsx b/src/components/katalog/link-import.tsx index 2db3bed..88339d0 100644 --- a/src/components/katalog/link-import.tsx +++ b/src/components/katalog/link-import.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { useState, useTransition } from 'react'; +import { useEffect, useState, useTransition } from 'react'; import { Link2, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { previewSetFromLink, type LinkPreview } from '@/app/(app)/katalog/link-actions'; @@ -35,11 +35,14 @@ export function NewSetEntry({ manufacturers, themes, defaultEan, + focus, children, }: { manufacturers: Array<{ id: string; name: string }>; themes: Array<{ id: string; name: string; manufacturerId: string }>; defaultEan?: string; + /** Set by the plus menu: start in the link field or in the form. */ + focus?: 'link' | 'hand'; /** Shown instead of the form while there is no manufacturer yet. */ children?: React.ReactNode; }) { @@ -50,6 +53,13 @@ export function NewSetEntry({ const [existing, setExisting] = useState<{ id: string; label: string } | null>(null); const [pending, startTransition] = useTransition(); + useEffect(() => { + if (focus !== 'hand') return; + const field = document.getElementById('setNumber'); + field?.scrollIntoView({ block: 'center' }); + field?.focus({ preventScroll: true }); + }, [focus]); + function submit(event: React.FormEvent) { event.preventDefault(); setExisting(null); @@ -114,6 +124,7 @@ export function NewSetEntry({ onChange={(event) => setUrl(event.target.value)} placeholder="https://…" maxLength={2000} + autoFocus={focus === 'link'} /> + ) : ( + + )} + + + + Was kommt dazu? + Ein Set in den Katalog bringen – auf dem schnellsten Weg. + +
+ {actions.map((action) => ( + + ))} +
+
+ + + + + ); +} diff --git a/src/components/layout/app-header.tsx b/src/components/layout/app-header.tsx index 1d8b2fc..561ac87 100644 --- a/src/components/layout/app-header.tsx +++ b/src/components/layout/app-header.tsx @@ -1,10 +1,27 @@ +import { Suspense } from 'react'; import Link from 'next/link'; import { BrickLogo } from '@/components/brick/logo'; import { DesktopNav } from './bottom-nav'; -import { UserMenu } from './user-menu'; +import { CollectorSwitch, type CollectorOption } from './collector-switch'; import type { SessionUser } from '@/lib/session'; -export function AppHeader({ user }: { user: SessionUser }) { +function initials(name: string): string { + const parts = name.trim().split(/\s+/); + const letters = (parts[0]?.[0] ?? '') + (parts.length > 1 ? (parts.at(-1)?.[0] ?? '') : ''); + return letters.toUpperCase() || '?'; +} + +export function AppHeader({ + user, + collectors, + storedCollectorId, + legoImport, +}: { + user: SessionUser; + collectors: CollectorOption[]; + storedCollectorId: string | null; + legoImport: boolean; +}) { return (
@@ -16,9 +33,26 @@ export function AppHeader({ user }: { user: SessionUser }) { Bricktrack
- - + + {/* Account, design and master data: everything that is not daily use. */} + + {initials(user.name)} +
+ + {collectors.length > 1 ? ( +
+ {/* useSearchParams needs a boundary; the switch reads ?sammler=. */} + }> + + +
+ ) : null}
); } diff --git a/src/components/layout/bottom-nav.tsx b/src/components/layout/bottom-nav.tsx index ff1a9b5..1ea6f4a 100644 --- a/src/components/layout/bottom-nav.tsx +++ b/src/components/layout/bottom-nav.tsx @@ -2,17 +2,52 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { Boxes, Gift, LayoutGrid, User } from 'lucide-react'; +import { Boxes, Gift, House, LayoutGrid } from 'lucide-react'; import { cn } from 'cn'; +import { AddMenu } from './add-menu'; +/** + * Four places and the plus in the middle. Account, design and master data + * sit behind "Mehr" in the header - they are not part of the daily routine. + */ const items = [ + { href: '/', label: 'Start', icon: House }, { href: '/katalog', label: 'Katalog', icon: LayoutGrid }, { href: '/sammlung', label: 'Sammlung', icon: Boxes }, { href: '/wunschlisten', label: 'Wünsche', icon: Gift }, - { href: '/konto', label: 'Konto', icon: User }, ] as const; -export function BottomNav() { +function isActive(pathname: string, href: string) { + if (href === '/') return pathname === '/'; + return pathname === href || pathname.startsWith(`${href}/`); +} + +function NavLink({ item, pathname }: { item: (typeof items)[number]; pathname: string }) { + const active = isActive(pathname, item.href); + return ( + + {/* The active tab sits on a small plate, like a tile on a baseplate. */} + + + + {item.label} + + ); +} + +export function BottomNav({ legoImport }: { legoImport: boolean }) { const pathname = usePathname(); return ( @@ -20,49 +55,32 @@ export function BottomNav() { aria-label="Hauptnavigation" className="fixed inset-x-0 bottom-0 z-40 border-t-[length:max(1px,var(--edge-w))] border-border bg-card/95 pb-[env(safe-area-inset-bottom)] backdrop-blur md:hidden" > -