diff --git a/PLAN_NEXT.md b/PLAN_NEXT.md index 86024d1..f162c60 100644 --- a/PLAN_NEXT.md +++ b/PLAN_NEXT.md @@ -114,6 +114,19 @@ localStorage manager + toggle + inline `ColorSchemeScript` equivalent. RJSF swap was import-only; SSR smoke test proved all 10 intake labels + textarea/password/checkbox/slider widgets render. `npm run build` green. +## 0.4 Landing page (conversion structure + screenshot automation) + +StartPage rebuilt along the 8-block anatomy (hero with outcome headline, +fact bar, 3 benefits, real product screenshot, 3 steps, trust list, FAQ, +final CTA, footer mini) — one CTA everywhere (`/access`), honest copy +(no fake testimonials/logos/numbers; pricing section omitted, model open). +Centralized hosting in Germany per operator decision (not self-hosted). +`frontend/scripts/capture.mjs` + `npm run screenshots` (Playwright/Chromium +in the frontend container) regenerates `public/screenshots/*.png`; the +workflows shot auto-selects `intake_form` to show the generated RJSF form. +Vite dev `server.watch` polling enabled (Windows bind-mount events often +never reach the Linux watcher → stale modules otherwise). + ## 1. Locked decisions (do not re-ask) * Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7f0deb..0221c78 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", + "playwright": "^1.63.0", "typescript": "~5.6.0", "vite": "^6.0.0" } @@ -1949,6 +1950,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postcss": { "version": "8.5.28", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9189d2b..5781b8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "screenshots": "node scripts/capture.mjs" }, "dependencies": { "@mantine/core": "^9.6.1", @@ -23,6 +24,7 @@ "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.0", + "playwright": "^1.63.0", "typescript": "~5.6.0", "vite": "^6.0.0" } diff --git a/frontend/public/screenshots/hero-light.png b/frontend/public/screenshots/hero-light.png new file mode 100644 index 0000000..b3b8418 Binary files /dev/null and b/frontend/public/screenshots/hero-light.png differ diff --git a/frontend/public/screenshots/workflows-light.png b/frontend/public/screenshots/workflows-light.png new file mode 100644 index 0000000..4adcd3e Binary files /dev/null and b/frontend/public/screenshots/workflows-light.png differ diff --git a/frontend/scripts/capture.mjs b/frontend/scripts/capture.mjs new file mode 100644 index 0000000..5f8d856 --- /dev/null +++ b/frontend/scripts/capture.mjs @@ -0,0 +1,66 @@ +/* Repeatable landing-page screenshots (re-run after UI changes). + * + * Usage (frontend container, dev server running): + * npm run screenshots # light scheme (default) + * npm run screenshots -- --scheme=dark + * + * Captures real app states into public/screenshots/ (committed, so the + * landing page always shows current UI). Authenticated states are out of + * scope (magic-link login is not scriptable without SMTP). + */ + +import { mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "playwright"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const outDir = join(root, "public", "screenshots"); +const base = process.env.SCREENSHOT_BASE_URL ?? "http://localhost:3000"; +const schemeArg = process.argv.find((a) => a.startsWith("--scheme=")); +const scheme = schemeArg ? schemeArg.split("=")[1] : "light"; + +const shots = [ + { name: "hero", path: "/", fullPage: true }, + { + name: "workflows", + path: "/app/workflows", + fullPage: false, + selectFlow: "intake_form", + }, +]; + +const browser = await chromium.launch(); +try { + mkdirSync(outDir, { recursive: true }); + const context = await browser.newContext({ + viewport: { width: 1440, height: 900 }, + colorScheme: scheme === "dark" ? "dark" : "light", + }); + await context.addInitScript( + (value) => { + try { + localStorage.setItem("kirche-color-scheme", value); + } catch { + /* private mode */ + } + }, + scheme === "dark" ? "dark" : "light", + ); + const page = await context.newPage(); + for (const shot of shots) { + await page.goto(base + shot.path, { waitUntil: "networkidle" }); + await page.waitForTimeout(1500); + if (shot.selectFlow) { + // Show the generated RJSF form instead of the default first flow. + await page.locator('[role="combobox"]').first().click(); + await page.getByRole("option", { name: shot.selectFlow }).click(); + await page.waitForTimeout(2000); + } + const file = join(outDir, `${shot.name}-${scheme}.png`); + await page.screenshot({ path: file, fullPage: shot.fullPage }); + console.log(`wrote ${file}`); + } +} finally { + await browser.close(); +} diff --git a/frontend/src/pages/StartPage.tsx b/frontend/src/pages/StartPage.tsx index 441e146..42b55f0 100644 --- a/frontend/src/pages/StartPage.tsx +++ b/frontend/src/pages/StartPage.tsx @@ -1,71 +1,280 @@ import { Link } from "react-router-dom"; -import { Button, Card, Grid, Group, Text, Title } from "@mantine/core"; import { + Accordion, + Anchor, + Badge, + Button, + Card, + Grid, + Group, + Image, + List, + Stack, + Text, + ThemeIcon, + Timeline, + Title, +} from "@mantine/core"; +import { + IconCheck, IconClipboardCheck, + IconFileText, IconInbox, IconKey, + IconLock, + IconServer, IconSparkles, + IconUserCheck, } from "@tabler/icons-react"; import { useAuth } from "../auth"; -const actions = [ +const CTA_LABEL = "Zugang anfragen"; + +const facts = [ + { icon: , text: "Gehostet in Deutschland" }, + { icon: , text: "Login ohne Passwort" }, + { icon: , text: "Jeder Schritt protokolliert" }, +]; + +const benefits = [ { - to: "/access", - icon: , - title: "Zugang anfragen", - text: "Neu hier? Fordern Sie Ihren persönlichen Zugang an.", + icon: , + title: "In eigenen Worten", + text: "Mitarbeitende beschreiben Anliegen wie gewohnt — das System führt in verständlichen Schritten durch den Vorgang.", }, { - to: "/app/workflows", icon: , - title: "Workflows", - text: "Vorgänge über Formulare starten und verfolgen.", + title: "Regeln automatisch geprüft", + text: "Schwellen, Zuständigkeiten und Abläufe prüft die Software selbst. Routine läuft durch, ohne dass jemand klicken muss.", }, { - to: "/app/tasks", - icon: , - title: "Aufgaben", - text: "Eigene Läufe und offene Freigaben im Blick.", + icon: , + title: "Mensch nur wo nötig", + text: "Echte Entscheidungen landen mit allen Fakten bei der zuständigen Person — Freigabe oder Ablehnung inklusive Notiz.", + }, +]; + +const steps = [ + { + title: "Zugang anfragen & anmelden", + text: "Anfrage stellen, per E-Mail-Link einloggen — ganz ohne Passwort.", + }, + { + title: "Formular ausfüllen & starten", + text: "Vorgang wählen, Angaben machen, starten. Der Ablauf folgt automatisch.", + }, + { + title: "Ergebnis & Protokoll", + text: "Antwort erhalten, Freigaben erteilen — jeder Schritt bleibt nachvollziehbar.", + }, +]; + +const faqs = [ + { + q: "Was kostet Kirche Einfach?", + a: "Das Preismodell steht noch nicht fest. Stellen Sie eine Zugangsfrage — wir melden uns mit allen Details.", + }, + { + q: "Was muss bei uns installiert werden?", + a: "Nichts. Kirche Einfach läuft im Browser — auf Büro-PC, Tablet oder Smartphone.", + }, + { + q: "Wo liegen unsere Daten?", + a: "Auf Servern in Deutschland. Es gibt keine externen Aufrufe an Drittdienste.", + }, + { + q: "Wer sieht welche Daten?", + a: "Jede Gemeinde arbeitet in ihrem eigenen Mandanten. Innerhalb davon steuern Rollen (Mitglied, Verwaltung), wer was sieht und entscheidet.", + }, + { + q: "Brauchen wir Passwörter?", + a: "Nein. Die Anmeldung läuft über einen persönlichen Link per E-Mail — gültig für eine Sitzung, nichts zu merken, nichts zu vergessen.", }, ]; export default function StartPage() { const { me } = useAuth(); return ( -
- - - - Willkommen - - - Kirche Einfach unterstützt die Verwaltung Ihrer Gemeinde: Vorgänge - starten, Freigaben erteilen, Zugänge verwalten. + + {/* Hero */} + + Für Pfarrbüros und Kirchengemeinden + + Weniger Verwaltung. Mehr Zeit für Menschen. + + + Kirche Einfach führt Mitarbeitende in eigenen Worten durch Vorgänge, + prüft Regeln automatisch und fragt Menschen nur, wo Urteil nötig + ist — lückenlos protokolliert. + + + + {me && ( - + Angemeldet als {me.email} ({me.tenant_id}). )} + + {/* Facts */} + + {facts.map((f) => ( + + + {f.icon} + + + {f.text} + + + ))} + + + {/* Benefits */} - {actions.map((a) => ( - + {benefits.map((b) => ( + - {a.icon} - - {a.title} + <ThemeIcon size={48} radius="md" variant="light" mb="sm"> + {b.icon} + </ThemeIcon> + <Title order={4} mb="xs"> + {b.title} - - {a.text} + + {b.text} - ))} -
+ + {/* Product visual */} + + + So sieht die Arbeit aus + + + Formulare entstehen automatisch aus den Vorgängen — hier das + Aufnahmeformular mit allen Feldtypen. + + Workflow-Formular in Kirche Einfach + + + {/* Steps */} + + + In drei Schritten dabei + + + {steps.map((s, i) => ( + + + {s.text} + + + ))} + + + + {/* Trust */} + + + Darauf können Sie sich verlassen + + + + + } + > + + Datenhoheit: Betrieb auf Servern in Deutschland, + ohne Aufrufe an Drittdienste. + + + Klare Zuständigkeit: Rollen und Mandanten trennen, + wer sieht und wer entscheidet. + + + Sicher ohne Passwort: Einmal-Links per E-Mail + statt vergessener Kennwörter. + + + Nachvollziehbar: Läufe, Entscheidungen und Notizen + bleiben protokolliert. + + + + + {/* FAQ */} +
+ + Häufige Fragen + + + {faqs.map((f) => ( + + {f.q} + + {f.a} + + + ))} + +
+ + {/* Final CTA */} + + + + Bereit für weniger Papierkram? + + + Stellen Sie eine Zugangsfrage — wir melden uns mit den nächsten + Schritten. + + + + + {/* Footer mini (Kontakt/Impressum liefert der Betreiber) */} + + + Kirche Einfach + + + Zugang + + + Login + + + {/* TODO Betreiber: Kontakt und Impressum ergänzen */} + Kontakt & Impressum folgen + + + + + + ); } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 75eeef7..4828119 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -7,6 +7,13 @@ export default defineConfig({ plugins: [react()], server: { port: 3000, + // Polling: the repo is bind-mounted from a Windows host, whose file + // events often never reach the Linux watcher — without this, edits + // (local or via VS Code) silently keep serving stale modules. + watch: { + usePolling: true, + interval: 1000, + }, proxy: { "/api": { target: "http://api:8000",