diff --git a/docs/product-screenshots.md b/docs/product-screenshots.md new file mode 100644 index 0000000..8ed9507 --- /dev/null +++ b/docs/product-screenshots.md @@ -0,0 +1,18 @@ +# Produkt-Screenshots + +Die Bilder in `public/landing/screenshots/` sind echte Browseraufnahmen der WebApp mit ausschließlich fiktiven Beispieldaten. Sie wurden am 13. Oktober 2026 aufgenommen. Keine Produktionsdaten, echten Kontaktadressen oder Zugangsdaten sind enthalten. + +Die Metadaten und Bildbeschreibungen stehen in `src/lib/marketing/screenshots.ts`. Landingpage und Featureseiten verwenden dieselben Dateien. Die Vorschau reserviert den Bildbereich, lädt Bilder bedarfsgerecht über Next Image und bietet eine tastaturbedienbare Vollansicht mit Fensteranpassung, Originalgröße und Scrollbereich. + +## Aufnahmen erneuern + +1. Eine **leere**, ausschließlich lokale PostgreSQL-Datenbank namens `kita_screenshots_test` erstellen. Andere Datenbanknamen oder Hosts werden vom Fixture-Skript abgewiesen; bestehende Daten werden niemals gelöscht. +2. `DATABASE_URL` nur für diese Sitzung auf diese Datenbank setzen und `npm run db:migrate:deploy` ausführen. +3. Mit derselben URL `npx tsx scripts/seed-screenshots.ts` ausführen. Das Skript legt Kita Sonnenwinkel, sechs fiktive Familien und eine Fachkraft an. Die Zugangsdaten werden nur lokal ausgegeben. Alle E-Mail-Adressen verwenden die nicht zustellbare Domain `example.invalid`. +4. Die App mit einer eigenen lokalen Auth-URL und einem Test-Secret starten. E-Mail-Versand und Analytics deaktivieren (`RESEND_API_KEY`, `NEXT_PUBLIC_UMAMI_SCRIPT_URL` und `NEXT_PUBLIC_UMAMI_WEBSITE_ID` leer; öffentliche Variablen schon beim Build leer setzen). Keine Jobs starten. +5. Über die normale Anmeldung und Navigation die Ansichten aufnehmen. Der Fixture-Stand verwendet Oktober/November 2026. Bei späteren Aufnahmen die Termine, Auswahl und Arbeitswoche passend aktualisieren. +6. Browseraufnahmen als JPEG speichern. Die Elternansichten wurden bei 1600 × 1000, die Arbeitszeitansicht bei 1600 × 1280 aufgenommen. Der Browser kann beim Ausschließen der Scrollbar leicht abweichende tatsächliche Bildmaße liefern; die Metadaten immer aus den **gespeicherten Dateien** lesen und aktualisieren. +7. Notdienst, Kalender, Adressbuch und eigene Abwesenheiten zeigen die Elternansicht. Bei Mitteilungen die erste aktualisierte Meldung öffnen. Für Arbeitszeit mit dem Teamkonto anmelden und zur erfassten Woche vom 5. bis 11. Oktober wechseln. +8. Bilder und Vergrößerung auf Desktop und Mobilgerät prüfen, dann Beispieldatenbank und lokalen Server entfernen. + +Die Screenshot-Files sind zusammen unter 500 KiB groß. Das alte Bild `notdienst-calendar-linen.png` wird nicht mehr von der Landingpage verwendet. diff --git a/public/landing/screenshots/abwesenheiten.jpg b/public/landing/screenshots/abwesenheiten.jpg new file mode 100644 index 0000000..a07ef77 Binary files /dev/null and b/public/landing/screenshots/abwesenheiten.jpg differ diff --git a/public/landing/screenshots/adressbuch.jpg b/public/landing/screenshots/adressbuch.jpg new file mode 100644 index 0000000..d460d9f Binary files /dev/null and b/public/landing/screenshots/adressbuch.jpg differ diff --git a/public/landing/screenshots/kalender.jpg b/public/landing/screenshots/kalender.jpg new file mode 100644 index 0000000..78b640d Binary files /dev/null and b/public/landing/screenshots/kalender.jpg differ diff --git a/public/landing/screenshots/mitteilungen.jpg b/public/landing/screenshots/mitteilungen.jpg new file mode 100644 index 0000000..bbd0fec Binary files /dev/null and b/public/landing/screenshots/mitteilungen.jpg differ diff --git a/public/landing/screenshots/notdienst.jpg b/public/landing/screenshots/notdienst.jpg new file mode 100644 index 0000000..6970240 Binary files /dev/null and b/public/landing/screenshots/notdienst.jpg differ diff --git a/public/landing/screenshots/personal.jpg b/public/landing/screenshots/personal.jpg new file mode 100644 index 0000000..409d8d1 Binary files /dev/null and b/public/landing/screenshots/personal.jpg differ diff --git a/scripts/seed-screenshots.ts b/scripts/seed-screenshots.ts new file mode 100644 index 0000000..2bf8aea --- /dev/null +++ b/scripts/seed-screenshots.ts @@ -0,0 +1,92 @@ +/** Synthetic fixture for public product screenshots. Never run against real data. */ +import { PrismaClient, UserRole, TerminType, type User, type Child } from "@prisma/client"; +import { hash } from "bcryptjs"; + +const url = new URL(process.env.DATABASE_URL ?? "postgresql://invalid"); +if (!['localhost', '127.0.0.1'].includes(url.hostname) || + url.pathname !== '/kita_screenshots_test') { + throw new Error('Screenshot fixtures require the isolated local kita_screenshots_test database.'); +} +const prisma = new PrismaClient(); +const date = (value: string) => new Date(`${value}T00:00:00Z`); + +async function main() { + if (await prisma.kita.count() || await prisma.user.count()) { + throw new Error('Database must be empty; existing data will never be reset.'); + } + const passwordHash = await hash('demo-screenshots-only', 10); + await prisma.$transaction(async (tx) => { + const kita = await tx.kita.create({ data: { name: 'Kita Sonnenwinkel', slug: 'sonnenwinkel-demo' } }); + const kitaId = kita.id; + const common = { kitaId, passwordHash, emailVerifiedAt: date('2026-09-01'), + privacyPolicyAcceptedAt: date('2026-09-01'), privacyPolicyVersion: '2026-05-01' }; + const admin = await tx.user.create({ data: { ...common, email: 'admin@example.invalid', firstName: 'Mara', lastName: 'Winter', role: UserRole.ADMIN } }); + const households = [ + ['Berger', 'Lena', 'Jonas', 'Emma', 'Leo'], + ['Hoffmann', 'Anna', 'Tim', 'Mila'], + ['Schneider', 'Clara', 'Ben', 'Paul'], + ['Neumann', 'Nora', 'Felix', 'Lotta'], + ['Weber', 'Sarah', 'David', 'Finn'], + ['Koch', 'Julia', 'Max', 'Ella'], + ]; + const parents: User[] = []; + const children: Child[] = []; + for (const [lastName, firstName, partner, ...childNames] of households) { + const family = await tx.family.create({ data: { kitaId, name: `Familie ${lastName}` } }); + for (const name of [firstName, partner]) { + parents.push(await tx.user.create({ data: { ...common, familyId: family.id, firstName: name, lastName, + email: `${name.toLowerCase()}@example.invalid`, role: UserRole.ELTERN, + directoryOptInAt: date('2026-09-01'), city: 'Musterstadt' } })); + } + for (const name of childNames) { + children.push(await tx.child.create({ data: { kitaId, familyId: family.id, firstName: name, lastName } })); + } + } + await tx.notdienstAvailability.createMany({ data: [2, 4, 6, 10, 12, 17, 20, 25].map(day => ({ + kitaId, childId: children[0].id, userId: parents[0].id, + date: date(`2026-11-${String(day).padStart(2, '0')}`), + })) }); + await tx.absence.createMany({ data: [ + { kitaId, childId: children[0].id, startDate: date('2026-10-12'), endDate: date('2026-10-14'), reason: 'ILLNESS' }, + { kitaId, childId: children[1].id, startDate: date('2026-10-26'), endDate: date('2026-10-30'), reason: 'VACATION', note: 'Familienurlaub in den Herbstferien' }, + ] }); + const events: [string, TerminType, string, string, boolean][] = [ + ['Elterncafé', 'ELTERNCAFE', '2026-10-08T13:00:00Z', '2026-10-08T14:30:00Z', false], + ['Ausflug in den Stadtwald', 'SONSTIGES', '2026-10-15T07:00:00Z', '2026-10-15T11:00:00Z', false], + ['Elternabend', 'ELTERNABEND', '2026-10-20T16:00:00Z', '2026-10-20T18:00:00Z', false], + ['Herbstfest im Garten', 'KITA_FEST', '2026-10-23T13:00:00Z', '2026-10-23T16:00:00Z', false], + ['Teamtag · Kita geschlossen', 'TEAMTAG', '2026-10-30T00:00:00Z', '2026-10-30T23:59:00Z', true], + ['Elterncafé', 'ELTERNCAFE', '2026-11-05T14:00:00Z', '2026-11-05T15:30:00Z', false], + ['Laternenfest', 'KITA_FEST', '2026-11-11T15:00:00Z', '2026-11-11T17:00:00Z', false], + ['Gartenaktion', 'MITMACH_TAG', '2026-11-14T09:00:00Z', '2026-11-14T12:00:00Z', false], + ['Teamtag · Kita geschlossen', 'TEAMTAG', '2026-11-23T00:00:00Z', '2026-11-23T23:59:00Z', true], + ]; + for (const [title, type, start, end, allDay] of events) { + const event = await tx.termin.create({ data: { kitaId, title, type, status: 'CONFIRMED', startDate: new Date(start), endDate: new Date(end), allDay, + description: title.includes('Herbstfest') ? 'Wir feiern gemeinsam mit allen Familien. Für Snacks und Getränke gibt es eine Mitbringliste.' : 'Gemeinsam planen wir unseren Kita-Alltag.', + createdById: admin.id, approvedById: admin.id, approvedAt: date('2026-10-01'), + requiresRsvp: type === 'KITA_FEST', rsvpMode: 'FAMILY', mitbringselListEnabled: type === 'KITA_FEST' } }); + if (type === 'KITA_FEST') { + await tx.familyEventParticipation.create({ data: { kitaId, eventId: event.id, familyId: parents[0].familyId!, status: 'ATTENDING', adultCount: 2 } }); + await tx.mitbringselItem.createMany({ data: parents.slice(0, 3).map((parent, index) => ({ kitaId, terminId: event.id, userId: parent.id, content: ['Apfelkuchen', 'Gemüsesticks', 'Eine Kanne Tee'][index] })) }); + } + } + for (const [title, content, created, updated] of [ + ['Herbstfest: Wir freuen uns auf euch!', 'Am **Freitag, 23. Oktober**, feiern wir ab 15 Uhr im Kita-Garten.\n\nBitte tragt euch im Kalender für die Teilnahme und in die Mitbringliste ein. Gemeinsam sorgen wir für ein buntes Buffet.\n\n**Aktualisierung:** Bei Regen feiern wir im Gruppenraum.', '2026-10-09', '2026-10-13'], + ['Elternabend am 20. Oktober', 'Wir treffen uns um **18 Uhr** im großen Gruppenraum.\n\nAuf der Agenda stehen der Kita-Alltag, die nächsten Ausflüge und eure Fragen. Wir freuen uns auf den Austausch!', '2026-10-08', '2026-10-08'], + ['Gemeinsam unseren Garten vorbereiten', 'Am 14. November machen wir unseren Garten winterfest. Kleine und große Helfer sind willkommen!\n\nHandschuhe und gute Laune reichen. Alle Details findet ihr im Kalender.', '2026-10-06', '2026-10-06'], + ]) { + await tx.announcement.create({ data: { kitaId, authorId: admin.id, title, content, audience: 'ALLE', + createdAt: date(created), updatedAt: date(updated), revision: created === updated ? 1 : 2, editedAt: created === updated ? null : date(updated) } }); + } + const staff = await tx.user.create({ data: { ...common, email: 'team@example.invalid', firstName: 'Sophie', lastName: 'Lind', role: UserRole.ERZIEHER } }); + const educator = await tx.educator.create({ data: { kitaId, firstName: staff.firstName, lastName: staff.lastName } }); + const profile = await tx.staffProfile.create({ data: { kitaId, educatorId: educator.id, userId: staff.id, + employmentStart: date('2026-01-01'), trackingStart: date('2026-10-01'), openingMinutes: 120, + workPatterns: { create: { effectiveFrom: date('2026-01-01'), dailyMinutes: [420,420,420,420,420,0,0] } }, + leaveAccounts: { create: { year: 2026, entitlementUnits: 60, carryUnits: 0 } } } }); + await tx.staffTimeEntry.createMany({ data: [5,6,7,8,9].map(day => ({ profileId: profile.id, date: date(`2026-10-${String(day).padStart(2,'0')}`), startMinute: 480, endMinute: 930, breakMinutes: 30 })) }); + }, { timeout: 30000 }); + console.log('Synthetic screenshot data ready. Logins: lena@example.invalid / team@example.invalid. Password: demo-screenshots-only.'); +} +main().finally(() => prisma.$disconnect()); diff --git a/src/app/features/[slug]/page.tsx b/src/app/features/[slug]/page.tsx index 75e3712..74a11b2 100644 --- a/src/app/features/[slug]/page.tsx +++ b/src/app/features/[slug]/page.tsx @@ -1,3 +1,5 @@ +import { ScreenshotPreview } from "@/components/marketing/product-screenshots"; +import { getProductScreenshot } from "@/lib/marketing/screenshots"; import { StructuredData } from "@/components/marketing/structured-data"; import { featureStructuredData, publicPageMetadata } from "@/lib/marketing/seo"; import type { Metadata } from "next"; @@ -66,6 +68,7 @@ export default async function FeaturePage({ params }: FeaturePageProps) { } const Icon = feature.icon; + const screenshot = getProductScreenshot(slug); return (
- Der Notdienst-Planer sammelt Verfügbarkeiten, erinnert an fehlende - Einträge und macht aus Antworten einen veröffentlichbaren - Monatsplan. Bei manuellen Änderungen bleibt sichtbar, welche - Haushalte für den Tag verfügbar sind. + Entdeckt die sechs Bereiche direkt in der App: vom Notdienst + über Kalender und Mitteilungen bis zur Arbeitszeit. Die Ansichten + zeigen Beispieldaten und lassen sich für einen genaueren Blick + vergrößern.
-