feat: conversion landing page with automated screenshots
This commit is contained in:
parent
85c5a8381c
commit
82f7ff8b02
8 changed files with 362 additions and 35 deletions
13
PLAN_NEXT.md
13
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 +
|
RJSF swap was import-only; SSR smoke test proved all 10 intake labels +
|
||||||
textarea/password/checkbox/slider widgets render. `npm run build` green.
|
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)
|
## 1. Locked decisions (do not re-ask)
|
||||||
|
|
||||||
* Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`,
|
* Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`,
|
||||||
|
|
|
||||||
30
frontend/package-lock.json
generated
30
frontend/package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"playwright": "^1.63.0",
|
||||||
"typescript": "~5.6.0",
|
"typescript": "~5.6.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1949,6 +1950,35 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.28",
|
"version": "8.5.28",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"screenshots": "node scripts/capture.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^9.6.1",
|
"@mantine/core": "^9.6.1",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"playwright": "^1.63.0",
|
||||||
"typescript": "~5.6.0",
|
"typescript": "~5.6.0",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
frontend/public/screenshots/hero-light.png
Normal file
BIN
frontend/public/screenshots/hero-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
BIN
frontend/public/screenshots/workflows-light.png
Normal file
BIN
frontend/public/screenshots/workflows-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
66
frontend/scripts/capture.mjs
Normal file
66
frontend/scripts/capture.mjs
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -1,71 +1,280 @@
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Button, Card, Grid, Group, Text, Title } from "@mantine/core";
|
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
Anchor,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
List,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Timeline,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
IconClipboardCheck,
|
IconClipboardCheck,
|
||||||
|
IconFileText,
|
||||||
IconInbox,
|
IconInbox,
|
||||||
IconKey,
|
IconKey,
|
||||||
|
IconLock,
|
||||||
|
IconServer,
|
||||||
IconSparkles,
|
IconSparkles,
|
||||||
|
IconUserCheck,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useAuth } from "../auth";
|
import { useAuth } from "../auth";
|
||||||
|
|
||||||
const actions = [
|
const CTA_LABEL = "Zugang anfragen";
|
||||||
|
|
||||||
|
const facts = [
|
||||||
|
{ icon: <IconServer size={20} />, text: "Gehostet in Deutschland" },
|
||||||
|
{ icon: <IconKey size={20} />, text: "Login ohne Passwort" },
|
||||||
|
{ icon: <IconClipboardCheck size={20} />, text: "Jeder Schritt protokolliert" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const benefits = [
|
||||||
{
|
{
|
||||||
to: "/access",
|
icon: <IconFileText size={28} />,
|
||||||
icon: <IconKey size={28} />,
|
title: "In eigenen Worten",
|
||||||
title: "Zugang anfragen",
|
text: "Mitarbeitende beschreiben Anliegen wie gewohnt — das System führt in verständlichen Schritten durch den Vorgang.",
|
||||||
text: "Neu hier? Fordern Sie Ihren persönlichen Zugang an.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: "/app/workflows",
|
|
||||||
icon: <IconSparkles size={28} />,
|
icon: <IconSparkles size={28} />,
|
||||||
title: "Workflows",
|
title: "Regeln automatisch geprüft",
|
||||||
text: "Vorgänge über Formulare starten und verfolgen.",
|
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: <IconUserCheck size={28} />,
|
||||||
icon: <IconInbox size={28} />,
|
title: "Mensch nur wo nötig",
|
||||||
title: "Aufgaben",
|
text: "Echte Entscheidungen landen mit allen Fakten bei der zuständigen Person — Freigabe oder Ablehnung inklusive Notiz.",
|
||||||
text: "Eigene Läufe und offene Freigaben im Blick.",
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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() {
|
export default function StartPage() {
|
||||||
const { me } = useAuth();
|
const { me } = useAuth();
|
||||||
return (
|
return (
|
||||||
<div>
|
<Stack gap="xl">
|
||||||
<Card withBorder padding="xl" mb="lg">
|
{/* Hero */}
|
||||||
<Group gap="sm" mb="xs">
|
<Card withBorder padding="xl" radius="lg">
|
||||||
<IconClipboardCheck size={32} />
|
<Badge mb="sm">Für Pfarrbüros und Kirchengemeinden</Badge>
|
||||||
<Title order={2}>Willkommen</Title>
|
<Title order={1} mb="sm">
|
||||||
</Group>
|
Weniger Verwaltung. Mehr Zeit für Menschen.
|
||||||
<Text>
|
</Title>
|
||||||
Kirche Einfach unterstützt die Verwaltung Ihrer Gemeinde: Vorgänge
|
<Text size="lg" c="dimmed" mb="lg" maw={640}>
|
||||||
starten, Freigaben erteilen, Zugänge verwalten.
|
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.
|
||||||
</Text>
|
</Text>
|
||||||
|
<Group>
|
||||||
|
<Button component={Link} to="/access" size="lg">
|
||||||
|
{CTA_LABEL}
|
||||||
|
</Button>
|
||||||
|
<Button component={Link} to="/login" size="lg" variant="light">
|
||||||
|
Zum Login
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
{me && (
|
{me && (
|
||||||
<Text mt="sm">
|
<Text mt="md">
|
||||||
Angemeldet als <strong>{me.email}</strong> ({me.tenant_id}).
|
Angemeldet als <strong>{me.email}</strong> ({me.tenant_id}).
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Facts */}
|
||||||
|
<Group gap="lg">
|
||||||
|
{facts.map((f) => (
|
||||||
|
<Group key={f.text} gap="xs">
|
||||||
|
<ThemeIcon variant="light" size="lg">
|
||||||
|
{f.icon}
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{f.text}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Benefits */}
|
||||||
<Grid>
|
<Grid>
|
||||||
{actions.map((a) => (
|
{benefits.map((b) => (
|
||||||
<Grid.Col key={a.to} span={{ base: 12, sm: 4 }}>
|
<Grid.Col key={b.title} span={{ base: 12, sm: 4 }}>
|
||||||
<Card withBorder padding="lg" h="100%">
|
<Card withBorder padding="lg" h="100%">
|
||||||
{a.icon}
|
<ThemeIcon size={48} radius="md" variant="light" mb="sm">
|
||||||
<Title order={4} mt="sm">
|
{b.icon}
|
||||||
{a.title}
|
</ThemeIcon>
|
||||||
|
<Title order={4} mb="xs">
|
||||||
|
{b.title}
|
||||||
</Title>
|
</Title>
|
||||||
<Text size="sm" c="dimmed" mt="xs" mb="md">
|
<Text size="sm" c="dimmed">
|
||||||
{a.text}
|
{b.text}
|
||||||
</Text>
|
</Text>
|
||||||
<Button component={Link} to={a.to} variant="light" fullWidth>
|
|
||||||
Öffnen
|
|
||||||
</Button>
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
|
||||||
|
{/* Product visual */}
|
||||||
|
<Card withBorder padding="lg" radius="lg">
|
||||||
|
<Title order={3} mb="xs">
|
||||||
|
So sieht die Arbeit aus
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed" mb="md">
|
||||||
|
Formulare entstehen automatisch aus den Vorgängen — hier das
|
||||||
|
Aufnahmeformular mit allen Feldtypen.
|
||||||
|
</Text>
|
||||||
|
<Image
|
||||||
|
src="/screenshots/workflows-light.png"
|
||||||
|
alt="Workflow-Formular in Kirche Einfach"
|
||||||
|
radius="md"
|
||||||
|
bd="1px solid var(--mantine-color-default-border)"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Steps */}
|
||||||
|
<Card withBorder padding="lg" radius="lg">
|
||||||
|
<Title order={3} mb="md">
|
||||||
|
In drei Schritten dabei
|
||||||
|
</Title>
|
||||||
|
<Timeline bulletSize={28}>
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<Timeline.Item
|
||||||
|
key={s.title}
|
||||||
|
bullet={i + 1}
|
||||||
|
title={s.title}
|
||||||
|
>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{s.text}
|
||||||
|
</Text>
|
||||||
|
</Timeline.Item>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Trust */}
|
||||||
|
<Card withBorder padding="lg" radius="lg">
|
||||||
|
<Title order={3} mb="md">
|
||||||
|
Darauf können Sie sich verlassen
|
||||||
|
</Title>
|
||||||
|
<List
|
||||||
|
spacing="sm"
|
||||||
|
icon={
|
||||||
|
<ThemeIcon size={24} radius="xl" variant="light">
|
||||||
|
<IconCheck size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List.Item>
|
||||||
|
<strong>Datenhoheit:</strong> Betrieb auf Servern in Deutschland,
|
||||||
|
ohne Aufrufe an Drittdienste.
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<strong>Klare Zuständigkeit:</strong> Rollen und Mandanten trennen,
|
||||||
|
wer sieht und wer entscheidet.
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<strong>Sicher ohne Passwort:</strong> Einmal-Links per E-Mail
|
||||||
|
statt vergessener Kennwörter.
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<strong>Nachvollziehbar:</strong> Läufe, Entscheidungen und Notizen
|
||||||
|
bleiben protokolliert.
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* FAQ */}
|
||||||
|
<div>
|
||||||
|
<Title order={3} mb="md">
|
||||||
|
Häufige Fragen
|
||||||
|
</Title>
|
||||||
|
<Accordion variant="separated">
|
||||||
|
{faqs.map((f) => (
|
||||||
|
<Accordion.Item key={f.q} value={f.q}>
|
||||||
|
<Accordion.Control>{f.q}</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<Text size="sm">{f.a}</Text>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Final CTA */}
|
||||||
|
<Card withBorder padding="xl" radius="lg">
|
||||||
|
<Group gap="sm" mb="xs">
|
||||||
|
<IconInbox size={28} />
|
||||||
|
<Title order={3}>Bereit für weniger Papierkram?</Title>
|
||||||
|
</Group>
|
||||||
|
<Text c="dimmed" mb="md">
|
||||||
|
Stellen Sie eine Zugangsfrage — wir melden uns mit den nächsten
|
||||||
|
Schritten.
|
||||||
|
</Text>
|
||||||
|
<Button component={Link} to="/access" size="lg">
|
||||||
|
{CTA_LABEL}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Footer mini (Kontakt/Impressum liefert der Betreiber) */}
|
||||||
|
<Group gap="md" mt="md">
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Kirche Einfach
|
||||||
|
</Text>
|
||||||
|
<Anchor component={Link} to="/access" size="xs">
|
||||||
|
Zugang
|
||||||
|
</Anchor>
|
||||||
|
<Anchor component={Link} to="/login" size="xs">
|
||||||
|
Login
|
||||||
|
</Anchor>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{/* TODO Betreiber: Kontakt und Impressum ergänzen */}
|
||||||
|
Kontakt & Impressum folgen
|
||||||
|
</Text>
|
||||||
|
<ThemeIcon size="sm" variant="transparent" aria-label="Sicherer Zugang">
|
||||||
|
<IconLock size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@ export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
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: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: "http://api:8000",
|
target: "http://api:8000",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue