diff --git a/e2e/screenshots.spec.ts b/e2e/screenshots.spec.ts index c0b784e..93f99be 100644 --- a/e2e/screenshots.spec.ts +++ b/e2e/screenshots.spec.ts @@ -19,6 +19,9 @@ function shotPath(name: string) { async function shoot(page: Page, name: string) { // Let fonts and images settle so the picture is not half-rendered. await page.waitForLoadState('networkidle'); + // A fixed bottom bar would be painted where the first viewport ends, in the + // middle of a full-page picture. In flow it lands at the end, where it belongs. + await page.addStyleTag({ content: '.fixed.bottom-0 { position: static !important; }' }); await page.screenshot({ path: shotPath(name), fullPage: true }); } @@ -117,6 +120,8 @@ test('Screenshots fuer die Anleitung', async ({ page, browser }) => { await visitor.getByRole('button', { name: 'Reservieren' }).click(); await expect(visitor.getByText('Reservierung gespeichert')).toBeVisible(); await visitor.getByRole('button', { name: 'Alles klar' }).click(); + // Otherwise the picture catches the dialog halfway through fading out. + await expect(visitor.getByRole('dialog')).toBeHidden(); await shoot(visitor, '14-reserviert'); await visitorContext.close(); @@ -129,6 +134,18 @@ test('Screenshots fuer die Anleitung', async ({ page, browser }) => { await expect(page.getByText('Passkeys', { exact: true })).toBeVisible(); await shoot(page, '16-konto-passkeys'); + // The same start page in the dark design, then back to the default. + await page.getByText('Nachtblau').click(); + await expect(page.locator('html')).toHaveAttribute('data-design', 'nacht'); + // The look switches at once; the cookie follows with the server action. + await page.waitForLoadState('networkidle'); + await visit(page, '/'); + await expect(page.locator('html')).toHaveAttribute('data-design', 'nacht'); + await shoot(page, '25-design-nachtblau'); + await visit(page, '/konto'); + await page.getByText('Klemmbaustein').click(); + await expect(page.locator('html')).toHaveAttribute('data-design', 'klemmbaustein'); + // --- 12. Lego import ------------------------------------------------------ // The screenshot container runs with a placeholder API key so the manual // shows the form, not the "not configured" hint. Nothing is submitted. diff --git a/package.json b/package.json index e593f1a..076b208 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,12 @@ }, "dependencies": { "@better-auth/passkey": "^1.7.3", + "@fontsource-variable/archivo": "^5.3.0", + "@fontsource-variable/bricolage-grotesque": "^5.3.0", + "@fontsource-variable/fredoka": "^5.3.0", + "@fontsource-variable/inter": "^5.3.0", + "@fontsource-variable/nunito": "^5.3.0", + "@fontsource-variable/outfit": "^5.3.0", "@zxing/browser": "^0.2.1", "@zxing/library": "^0.23.0", "better-auth": "^1.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8784d08..222e5fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,24 @@ importers: '@better-auth/passkey': specifier: ^1.7.3 version: 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.5.4))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.3(better-sqlite3@13.0.3)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@types/better-sqlite3@9.6.0)(better-sqlite3@13.0.3)(kysely@0.29.5))(next@15.5.25(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@20.19.43)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@5.0.0(@types/node@20.19.43)(vite@8.2.2(@types/node@20.19.43)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.13))))(better-call@1.4.0(zod@4.5.4))(nanostores@1.5.3) + '@fontsource-variable/archivo': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/bricolage-grotesque': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/fredoka': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/inter': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/nunito': + specifier: ^5.3.0 + version: 5.3.0 + '@fontsource-variable/outfit': + specifier: ^5.3.0 + version: 5.3.0 '@zxing/browser': specifier: ^0.2.1 version: 0.2.1(@zxing/library@0.23.0) @@ -887,6 +905,24 @@ packages: '@floating-ui/utils@0.2.12': resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@fontsource-variable/archivo@5.3.0': + resolution: {integrity: sha512-HogK8FJelrD1o7TlZlkIVtHgc20bO5PZRWE7mUeUTdMN055alznQV6/00J00IBeu8FQAH4s3zW9UJNvKExXf+g==} + + '@fontsource-variable/bricolage-grotesque@5.3.0': + resolution: {integrity: sha512-TLi9Q4hJjS2UvoTMRSS2nHu6c4R56lAw60NR9QYtVRCHn0XtsFpiEhNffZ8Glsoxu6wEEwLKBP8lb94J52PNBA==} + + '@fontsource-variable/fredoka@5.3.0': + resolution: {integrity: sha512-13w98iw2RcmYfZQRZPLSu+0YJK3HHm5A2XrpU26moFIAePmfiKfvkmEJUgkc8r5GNR+pbDijOueS1KdGAMAWmQ==} + + '@fontsource-variable/inter@5.3.0': + resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==} + + '@fontsource-variable/nunito@5.3.0': + resolution: {integrity: sha512-tw0ch7evIDSwoeUy+0XzocYHfbRWLeCRPJy22KgbrLjDLAbNw/fErBiil10Ssrs3Sjr55zaZrr36a975j8cBkQ==} + + '@fontsource-variable/outfit@5.3.0': + resolution: {integrity: sha512-fPb5XnYK0k7SbR++HEmm4+ZqDszmLKQ+sPRUr8oQ/KNC0vy6r6hazJFe8iYozv4cqlHbvJ/JECkJWrfDodUMVQ==} + '@hexagon/base64@1.1.28': resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} @@ -5659,6 +5695,18 @@ snapshots: '@floating-ui/utils@0.2.12': {} + '@fontsource-variable/archivo@5.3.0': {} + + '@fontsource-variable/bricolage-grotesque@5.3.0': {} + + '@fontsource-variable/fredoka@5.3.0': {} + + '@fontsource-variable/inter@5.3.0': {} + + '@fontsource-variable/nunito@5.3.0': {} + + '@fontsource-variable/outfit@5.3.0': {} + '@hexagon/base64@1.1.28': {} '@hono/node-server@2.1.1(hono@4.13.7)': diff --git a/src/app/(app)/katalog/[id]/page.tsx b/src/app/(app)/katalog/[id]/page.tsx index 8650504..527ec1d 100644 --- a/src/app/(app)/katalog/[id]/page.tsx +++ b/src/app/(app)/katalog/[id]/page.tsx @@ -1,7 +1,9 @@ import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { Check, ExternalLink, Gift, ImageOff, Pencil } from 'lucide-react'; +import { Check, ExternalLink, Gift, Pencil } from 'lucide-react'; +import { SetPlaceholder } from '@/components/brick/set-placeholder'; +import { Studs } from '@/components/brick/studs'; import { InstructionsSection } from '@/components/katalog/instructions-section'; import { PhotosSection } from '@/components/katalog/photos-section'; import { PriceSection } from '@/components/katalog/price-section'; @@ -114,15 +116,21 @@ export default async function SetDetailPage({ params, searchParams }: Props) {
Was möchtest du tun?
-+ {formatNumber(stats.setCount)} von {pluralize(totalSets, 'Set', 'Sets')} +
++ {formatNumber(stats.pieceCount)} +
+Teile verbaut
++ {formatEuro(stats.currentValueCents)} +
++ {stats.currentValueCents === stats.retailValueCents ? 'Wert nach UVP' : 'Aktueller Wert'} +
++ {nextEvent.title} +
++ {daysLabel(daysUntil(nextEvent.eventDate, now))} ·{' '} + {nextEvent.openCount === 1 + ? '1 Wunsch noch offen' + : `${formatNumber(nextEvent.openCount)} Wünsche noch offen`} +
+{list.note}
- ) : null} - -- Klick auf „Das schenke ich“, damit niemand dasselbe doppelt kauft. Eine Anmeldung - brauchst du dafür nicht. -
-- Auf dieser Liste steht gerade nichts. -
- ) : ( -{list.note}
: null} ++ Klick auf „Das schenke ich“, damit niemand dasselbe doppelt kauft. Eine Anmeldung + brauchst du dafür nicht. +
++ Auf dieser Liste steht gerade nichts. +
+ ) : ( +{set.name}
-{set.name}
+