Add shadcn UI foundation and update documentation

This commit is contained in:
Janpeter Visser 2026-04-18 14:39:22 +02:00
parent 7d443a004a
commit e7e151b439
28 changed files with 1333 additions and 589 deletions

View file

@ -8,6 +8,7 @@ Wellness-first webapp voor individuele gebruikers die hun energie willen plannen
- React 19
- TypeScript
- Tailwind CSS
- shadcn/ui component foundation
- Vercel als hostingdoel
- Supabase voor database en authenticatie
@ -42,6 +43,12 @@ Voor `ST-102` staat de eerste databasefundering in:
Voer deze SQL uit in de Supabase SQL Editor of via de Supabase CLI voordat je
de profile/settings-laag lokaal test.
## UI foundation
De app gebruikt nu `shadcn/ui` bovenop `Tailwind CSS` als herbruikbare basis voor
knoppen, formulieren, kaarten en meldingen. De theme tokens staan centraal in
`app/globals.css`, zodat kleur, focus-states en componentgedrag consistenter blijven.
## Eerstvolgende bouwstappen
1. `ST-201` Ochtendcheck-in UI bouwen

View file

@ -1,9 +1,19 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { signOutAction } from "@/app/auth-actions";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
import { getProfileBundleForCurrentUser } from "@/lib/profile/service";
import Link from "next/link";
import { cn } from "@/lib/utils";
export const dynamic = "force-dynamic";
@ -74,9 +84,11 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(167,201,87,0.22),_transparent_32%),linear-gradient(180deg,_#f5f4ee_0%,_#eef2e6_100%)] px-6 py-10 text-slate-900 sm:px-8">
<div className="mx-auto flex max-w-6xl flex-col gap-8">
{notice ? (
<div className="rounded-[1.5rem] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm leading-7 text-emerald-900">
{notice}
</div>
<Alert className="rounded-[1.5rem] border-emerald-200 bg-emerald-50 text-emerald-950 [&_svg]:text-emerald-700">
<AlertDescription className="leading-7 text-current">
{notice}
</AlertDescription>
</Alert>
) : null}
<header className="flex flex-col gap-5 rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:flex-row sm:items-start sm:justify-between sm:p-8">
@ -98,105 +110,125 @@ export default async function DashboardPage({ searchParams }: DashboardPageProps
<div className="flex flex-wrap items-center gap-3">
<Link
href="/settings"
className="inline-flex rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
className={cn(
buttonVariants({ variant: "outline", size: "lg" }),
"h-11 rounded-full px-5",
)}
>
Instellingen
</Link>
<button
type="submit"
className="inline-flex rounded-full border border-emerald-900/15 bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
<Button type="submit" size="lg" className="h-11 rounded-full px-5">
Uitloggen
</button>
</Button>
</div>
</form>
</header>
<section className="grid gap-5 md:grid-cols-3">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Auth
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
Cookie-based sessie actief
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Gebruiker-ID `{authState.userId}` is server-side gevalideerd via Supabase SSR-auth.
</p>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Auth
</p>
<CardTitle className="text-lg text-slate-900">Cookie-based sessie actief</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">
Gebruiker-ID `{authState.userId}` is server-side gevalideerd via Supabase SSR-auth.
</CardDescription>
</CardContent>
</Card>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Profiel
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
{profileTitle}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Taal `{profile.locale}` en timezone `{profile.timezone}` staan nu per
gebruiker opgeslagen.
</p>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Profiel
</p>
<CardTitle className="text-lg text-slate-900">{profileTitle}</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">
Taal `{profile.locale}` en timezone `{profile.timezone}` staan nu per
gebruiker opgeslagen.
</CardDescription>
</CardContent>
</Card>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Onboarding
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
{onboardingState}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Nieuwe accounts starten bewust zonder afgeronde onboarding, zodat
`ST-103` straks een duidelijke eerste flow kan aansturen.
</p>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Onboarding
</p>
<CardTitle className="text-lg text-slate-900">{onboardingState}</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">
Nieuwe accounts starten bewust zonder afgeronde onboarding, zodat
`ST-103` straks een duidelijke eerste flow kan aansturen.
</CardDescription>
</CardContent>
</Card>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Instellingen
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
Punten {formatToggleState(settings.showEnergyPoints, "zichtbaar", "verborgen")}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Ochtendreminder: {morningReminderState}. Reflectieprompts:{" "}
{formatToggleState(settings.reflectionReminderEnabled)}.
</p>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Instellingen
</p>
<CardTitle className="text-lg text-slate-900">
Punten {formatToggleState(settings.showEnergyPoints, "zichtbaar", "verborgen")}
</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">
Ochtendreminder: {morningReminderState}. Reflectieprompts:{" "}
{formatToggleState(settings.reflectionReminderEnabled)}.
</CardDescription>
</CardContent>
</Card>
</section>
{!profile.onboardingCompleted ? (
<section className="flex flex-col gap-4 rounded-[1.75rem] border border-amber-900/15 bg-amber-50 px-6 py-5 text-sm leading-7 text-amber-950 shadow-[0_12px_40px_rgba(146,64,14,0.08)] sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold">Je onboarding is nog niet afgerond.</p>
<p className="mt-1 max-w-2xl text-amber-900">
Je kunt de korte flow later alsnog afronden om je basisinstellingen
en eerste voorkeuren vast te leggen.
</p>
</div>
<Link
href="/onboarding"
className="inline-flex shrink-0 rounded-full bg-amber-950 px-5 py-3 text-sm font-semibold text-amber-50 transition hover:-translate-y-0.5 hover:bg-amber-900"
>
Rond onboarding af
</Link>
</section>
<Card className="rounded-[1.75rem] border border-amber-900/15 bg-amber-50 py-0 text-amber-950 shadow-[0_12px_40px_rgba(146,64,14,0.08)]">
<CardContent className="flex flex-col gap-4 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold">Je onboarding is nog niet afgerond.</p>
<p className="mt-1 max-w-2xl text-sm leading-7 text-amber-900">
Je kunt de korte flow later alsnog afronden om je basisinstellingen
en eerste voorkeuren vast te leggen.
</p>
</div>
<Link
href="/onboarding"
className={cn(
buttonVariants({ size: "lg" }),
"h-11 shrink-0 rounded-full bg-amber-950 px-5 text-amber-50 hover:bg-amber-900",
)}
>
Rond onboarding af
</Link>
</CardContent>
</Card>
) : (
<section className="flex flex-col gap-4 rounded-[1.75rem] border border-emerald-950/10 bg-emerald-950 px-6 py-5 text-sm leading-7 text-emerald-50 shadow-[0_12px_40px_rgba(6,78,59,0.18)] sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold">Je instellingen kun je nu ook los beheren.</p>
<p className="mt-1 max-w-2xl text-emerald-100/85">
`ST-104` staat nu klaar als aparte route, zodat je reminders,
timezone en zichtbaarheid van punten later zelfstandig kunt aanpassen.
</p>
</div>
<Link
href="/settings"
className="inline-flex shrink-0 rounded-full bg-white px-5 py-3 text-sm font-semibold text-emerald-950 transition hover:-translate-y-0.5 hover:bg-emerald-50"
>
Open instellingen
</Link>
</section>
<Card className="rounded-[1.75rem] border border-primary/10 bg-primary py-0 text-primary-foreground shadow-[0_12px_40px_rgba(22,58,43,0.18)]">
<CardContent className="flex flex-col gap-4 px-6 py-5 sm:flex-row sm:items-center sm:justify-between">
<div>
<p className="font-semibold">Je instellingen kun je nu ook los beheren.</p>
<p className="mt-1 max-w-2xl text-sm leading-7 text-primary-foreground/85">
`ST-104` staat nu klaar als aparte route, zodat je reminders,
timezone en zichtbaarheid van punten later zelfstandig kunt aanpassen.
</p>
</div>
<Link
href="/settings"
className={cn(
buttonVariants({ variant: "secondary", size: "lg" }),
"h-11 shrink-0 rounded-full px-5",
)}
>
Open instellingen
</Link>
</CardContent>
</Card>
)}
</div>
</main>

View file

@ -9,38 +9,38 @@
Palatino, Georgia, serif;
--font-body: "Inter", "Aptos", "Segoe UI", "Helvetica Neue", Arial,
sans-serif;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: #f5f4ee;
--foreground: #0f172a;
--card: rgb(255 255 255 / 0.84);
--card-foreground: #0f172a;
--popover: #ffffff;
--popover-foreground: #0f172a;
--primary: #163a2b;
--primary-foreground: #effaf3;
--secondary: #e5ecde;
--secondary-foreground: #163a2b;
--muted: #eef2e6;
--muted-foreground: #51606f;
--accent: #dbe7d1;
--accent-foreground: #163a2b;
--destructive: #b91c1c;
--border: rgb(15 23 42 / 0.1);
--input: rgb(15 23 42 / 0.12);
--ring: #5d8a67;
--chart-1: #163a2b;
--chart-2: #5d8a67;
--chart-3: #90a955;
--chart-4: #b7c5a4;
--chart-5: #d7dfce;
--radius: 1rem;
--sidebar: #fbfaf5;
--sidebar-foreground: #0f172a;
--sidebar-primary: #163a2b;
--sidebar-primary-foreground: #effaf3;
--sidebar-accent: #dbe7d1;
--sidebar-accent-foreground: #163a2b;
--sidebar-border: rgb(15 23 42 / 0.08);
--sidebar-ring: #5d8a67;
}
* {
@ -64,8 +64,8 @@ a {
}
@theme inline {
--font-heading: var(--font-sans);
--font-sans: var(--font-sans);
--font-heading: var(--font-display);
--font-sans: var(--font-body);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@ -107,47 +107,57 @@ a {
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--background: #111927;
--foreground: #eef6f0;
--card: rgb(17 25 39 / 0.84);
--card-foreground: #eef6f0;
--popover: #152131;
--popover-foreground: #eef6f0;
--primary: #d9f2de;
--primary-foreground: #133225;
--secondary: #243244;
--secondary-foreground: #eef6f0;
--muted: #243244;
--muted-foreground: #b1bec8;
--accent: #31485b;
--accent-foreground: #eef6f0;
--destructive: #ef4444;
--border: rgb(255 255 255 / 0.12);
--input: rgb(255 255 255 / 0.14);
--ring: #88b593;
--chart-1: #d9f2de;
--chart-2: #88b593;
--chart-3: #90a955;
--chart-4: #51606f;
--chart-5: #243244;
--sidebar: #152131;
--sidebar-foreground: #eef6f0;
--sidebar-primary: #d9f2de;
--sidebar-primary-foreground: #133225;
--sidebar-accent: #243244;
--sidebar-accent-foreground: #eef6f0;
--sidebar-border: rgb(255 255 255 / 0.12);
--sidebar-ring: #88b593;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
html {
@apply font-sans;
background: var(--background);
color: var(--foreground);
}
}
body {
@apply bg-background text-foreground;
margin: 0;
min-height: 100vh;
font-family: var(--font-body), sans-serif;
-webkit-font-smoothing: antialiased;
}
}

View file

@ -1,9 +1,5 @@
import type { Metadata } from "next";
import "./globals.css";
import { Geist } from "next/font/google";
import { cn } from "@/lib/utils";
const geist = Geist({subsets:['latin'],variable:'--font-sans'});
export const metadata: Metadata = {
title: "Inspannings Monitor",
@ -17,8 +13,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="nl" className={cn("font-sans", geist.variable)}>
<body>{children}</body>
<html lang="nl">
<body className="min-h-screen">{children}</body>
</html>
);
}

View file

@ -3,6 +3,10 @@ import { redirect } from "next/navigation";
import { AuthNotice } from "@/components/auth/auth-notice";
import { AuthPanel } from "@/components/auth/auth-panel";
import { signInAction } from "@/app/auth-actions";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getAuthNotice } from "@/lib/auth/messages";
import { buildPathWithQuery, sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
@ -53,42 +57,51 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
<AuthNotice notice={notice} />
{!authState.isConfigured ? (
<div className="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
</div>
<Alert className="rounded-[1.5rem] border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700">
<AlertDescription className="leading-7 text-current">
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
</AlertDescription>
</Alert>
) : (
<form action={signInAction} className="space-y-4">
<form action={signInAction} className="space-y-5">
<input type="hidden" name="next" value={next} />
<label className="block text-sm font-medium text-slate-800">
E-mailadres
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<div className="space-y-2">
<Label htmlFor="email" className="text-slate-800">
E-mailadres
</Label>
<Input
id="email"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
type="email"
name="email"
autoComplete="email"
required
/>
</label>
</div>
<label className="block text-sm font-medium text-slate-800">
Wachtwoord
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<div className="space-y-2">
<Label htmlFor="password" className="text-slate-800">
Wachtwoord
</Label>
<Input
id="password"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
type="password"
name="password"
autoComplete="current-password"
minLength={8}
required
/>
</label>
</div>
<button
<Button
type="submit"
className="inline-flex w-full items-center justify-center rounded-2xl bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
size="lg"
className="w-full rounded-[1.25rem]"
>
Inloggen
</button>
</Button>
</form>
)}
</AuthPanel>

View file

@ -1,13 +1,20 @@
import Link from "next/link";
import { signOutAction } from "@/app/auth-actions";
import { AuthNotice } from "@/components/auth/auth-notice";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { getAuthNotice } from "@/lib/auth/messages";
import { getAuthState } from "@/lib/auth/session";
import { cn } from "@/lib/utils";
export const dynamic = "force-dynamic";
const headerActionClassName =
"shrink-0 whitespace-nowrap rounded-full border border-black/10 bg-white/70 px-4 py-2 text-sm font-medium text-slate-900 shadow-sm transition hover:-translate-y-0.5";
const loopSteps = [
{
title: "Check-in",
@ -68,30 +75,36 @@ export default async function Home({ searchParams }: HomePageProps) {
<>
<Link
href="/dashboard"
className={headerActionClassName}
className={cn(
buttonVariants({ variant: "outline", size: "lg" }),
"h-11 shrink-0 whitespace-nowrap rounded-full px-5",
)}
>
Naar dashboard
</Link>
<form action={signOutAction}>
<button
type="submit"
className={headerActionClassName}
>
<Button type="submit" variant="outline" size="lg" className="h-11 shrink-0 whitespace-nowrap rounded-full px-5">
Uitloggen
</button>
</Button>
</form>
</>
) : (
<>
<Link
href="/login"
className={headerActionClassName}
className={cn(
buttonVariants({ variant: "outline", size: "lg" }),
"h-11 shrink-0 whitespace-nowrap rounded-full px-5",
)}
>
Inloggen
</Link>
<Link
href="/sign-up"
className={headerActionClassName}
className={cn(
buttonVariants({ variant: "outline", size: "lg" }),
"h-11 shrink-0 whitespace-nowrap rounded-full px-5",
)}
>
Account aanmaken
</Link>
@ -105,91 +118,97 @@ export default async function Home({ searchParams }: HomePageProps) {
</div>
</header>
{notice ? (
<div className="mb-6 rounded-[1.5rem] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm leading-7 text-emerald-900">
{notice.text}
</div>
) : null}
<AuthNotice notice={notice} />
<section className="grid gap-6 lg:grid-cols-[1.35fr_0.95fr]">
<article className="rounded-[2rem] border border-black/10 bg-white/70 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<p className="mb-4 max-w-2xl text-lg leading-8 text-slate-700">
<Card className="rounded-[2rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur">
<CardContent className="p-6 sm:p-8">
<p className="mb-4 max-w-2xl text-lg leading-8 text-slate-700">
De projectbasis staat nu, inclusief de eerste auth-laag via Supabase.
Release 1 blijft bewust smal: publieke landing, aparte login/signup
routes en een eerste protected dashboard als basis voor de volgende stories.
</p>
<div className="grid gap-4 md:grid-cols-3">
{loopSteps.map((step, index) => (
<section
key={step.title}
className="rounded-[1.5rem] border border-black/8 bg-stone-50 p-5"
>
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.22em] text-slate-500">
Stap {index + 1}
</p>
<h2 className="mb-2 font-[family-name:var(--font-display)] text-2xl">
{step.title}
</h2>
<p className="text-sm leading-7 text-slate-700">{step.copy}</p>
</section>
))}
</div>
</article>
</p>
<div className="grid gap-4 md:grid-cols-3">
{loopSteps.map((step, index) => (
<Card
key={step.title}
className="rounded-[1.5rem] border border-border/50 bg-background/80 py-0 shadow-none"
>
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground">
Stap {index + 1}
</p>
<CardTitle className="font-[family-name:var(--font-display)] text-2xl">
{step.title}
</CardTitle>
</CardHeader>
<CardContent className="pb-5">
<CardDescription className="text-sm leading-7 text-muted-foreground">
{step.copy}
</CardDescription>
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
<aside className="rounded-[2rem] border border-emerald-950/10 bg-emerald-950 px-6 py-7 text-emerald-50 shadow-[0_18px_60px_rgba(6,78,59,0.18)] sm:px-8">
<p className="mb-4 text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
Release 1 focus
</p>
<ul className="space-y-3">
<Card className="rounded-[2rem] border border-primary/10 bg-primary py-0 text-primary-foreground shadow-[0_18px_60px_rgba(22,58,43,0.18)]">
<CardHeader className="px-6 pt-7 sm:px-8">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/75">
Release 1 focus
</p>
</CardHeader>
<CardContent className="space-y-3 px-6 pb-7 sm:px-8">
{releaseFocus.map((item) => (
<li
<Card
key={item}
className="rounded-2xl border border-white/10 bg-white/8 px-4 py-3 text-sm leading-7"
className="rounded-[1.5rem] border border-white/10 bg-white/8 py-0 text-primary-foreground shadow-none"
>
{item}
</li>
<CardContent className="px-4 py-3 text-sm leading-7">{item}</CardContent>
</Card>
))}
</ul>
{authState.isConfigured ? (
<p className="mt-5 text-sm leading-7 text-emerald-100/80">
Auth is ingericht met e-mail, wachtwoord en verplichte e-mailverificatie.
</p>
) : (
<p className="mt-5 text-sm leading-7 text-emerald-100/80">
Voeg `.env.local` toe om login, signup en protected routes lokaal te activeren.
</p>
)}
</aside>
{authState.isConfigured ? (
<p className="pt-2 text-sm leading-7 text-primary-foreground/80">
Auth is ingericht met e-mail, wachtwoord en verplichte e-mailverificatie.
</p>
) : (
<p className="pt-2 text-sm leading-7 text-primary-foreground/80">
Voeg `.env.local` toe om login, signup en protected routes lokaal te activeren.
</p>
)}
</CardContent>
</Card>
</section>
<section className="mt-8 grid gap-5 rounded-[2rem] border border-black/10 bg-white/60 p-6 shadow-[0_10px_45px_rgba(71,85,105,0.08)] backdrop-blur sm:grid-cols-2 lg:grid-cols-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Volgende story
</p>
<p className="mt-2 font-semibold text-slate-900">
ST-201 Ochtendcheck-in
</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Doelgroep
</p>
<p className="mt-2 font-semibold text-slate-900">Volwassen individuele gebruikers</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Positionering
</p>
<p className="mt-2 font-semibold text-slate-900">Wellness / self-management</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Status
</p>
<p className="mt-2 font-semibold text-slate-900">Auth, onboarding en settings actief</p>
</div>
</section>
<Card className="mt-8 rounded-[2rem] border border-border/60 bg-card/80 py-0 shadow-[0_10px_45px_rgba(71,85,105,0.08)] backdrop-blur">
<CardContent className="grid gap-5 p-6 sm:grid-cols-2 lg:grid-cols-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Volgende story
</p>
<p className="mt-2 font-semibold text-slate-900">ST-201 Ochtendcheck-in</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Doelgroep
</p>
<p className="mt-2 font-semibold text-slate-900">Volwassen individuele gebruikers</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Positionering
</p>
<p className="mt-2 font-semibold text-slate-900">Wellness / self-management</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Status
</p>
<p className="mt-2 font-semibold text-slate-900">Auth, onboarding en settings actief</p>
</div>
</CardContent>
</Card>
</div>
</main>
);

View file

@ -2,9 +2,19 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import { signOutAction } from "@/app/auth-actions";
import { SettingsForm } from "@/components/settings/settings-form";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
import { getProfileBundleForCurrentUser } from "@/lib/profile/service";
import { cn } from "@/lib/utils";
export const dynamic = "force-dynamic";
@ -81,55 +91,63 @@ export default async function SettingsPage({ searchParams }: SettingsPageProps)
<div className="flex flex-wrap items-center gap-3">
<Link
href="/dashboard"
className="rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
className={cn(
buttonVariants({ variant: "outline", size: "lg" }),
"h-11 rounded-full px-5",
)}
>
Terug naar dashboard
</Link>
<form action={signOutAction}>
<button
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
<Button type="submit" size="lg" className="h-11 rounded-full px-5">
Uitloggen
</button>
</Button>
</form>
</div>
</header>
{notice ? (
<div className="rounded-[1.5rem] border border-emerald-200 bg-emerald-50 px-5 py-4 text-sm leading-7 text-emerald-900">
{notice}
</div>
<Alert className="rounded-[1.5rem] border-emerald-200 bg-emerald-50 text-emerald-950 [&_svg]:text-emerald-700">
<AlertDescription className="leading-7 text-current">
{notice}
</AlertDescription>
</Alert>
) : null}
<section className="grid gap-5 lg:grid-cols-[1.1fr_0.9fr]">
<SettingsForm profileBundle={profileBundle} />
<aside className="space-y-5">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Account
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
{profileTitle}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
E-mailadres: {profileBundle.profile.email ?? authState.email ?? "Onbekend"}
</p>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Account
</p>
<CardTitle className="text-lg text-slate-900">{profileTitle}</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">
E-mailadres: {profileBundle.profile.email ?? authState.email ?? "Onbekend"}
</CardDescription>
</CardContent>
</Card>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Huidige status
</p>
<p className="mt-3 text-lg font-semibold text-slate-900">
Onboarding {profileBundle.profile.onboardingCompleted ? "afgerond" : "later afronden"}
</p>
<p className="mt-3 text-sm leading-7 text-slate-700">
Je kunt later altijd terug naar onboarding of direct verder bouwen op
deze voorkeuren in de dagflow.
</p>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Huidige status
</p>
<CardTitle className="text-lg text-slate-900">
Onboarding {profileBundle.profile.onboardingCompleted ? "afgerond" : "later afronden"}
</CardTitle>
</CardHeader>
<CardContent className="pb-6">
<CardDescription className="text-sm leading-7 text-muted-foreground">
Je kunt later altijd terug naar onboarding of direct verder bouwen op
deze voorkeuren in de dagflow.
</CardDescription>
</CardContent>
</Card>
</aside>
</section>
</div>

View file

@ -3,6 +3,10 @@ import { redirect } from "next/navigation";
import { AuthNotice } from "@/components/auth/auth-notice";
import { AuthPanel } from "@/components/auth/auth-panel";
import { signUpAction } from "@/app/auth-actions";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getAuthNotice } from "@/lib/auth/messages";
import { buildPathWithQuery, sanitizeNextPath } from "@/lib/auth/navigation";
import { getAuthState } from "@/lib/auth/session";
@ -53,42 +57,51 @@ export default async function SignUpPage({ searchParams }: SignUpPageProps) {
<AuthNotice notice={notice} />
{!authState.isConfigured ? (
<div className="rounded-2xl border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
</div>
<Alert className="rounded-[1.5rem] border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700">
<AlertDescription className="leading-7 text-current">
Voeg eerst je Supabase-gegevens toe in `.env.local` op basis van `.env.example`.
</AlertDescription>
</Alert>
) : (
<form action={signUpAction} className="space-y-4">
<form action={signUpAction} className="space-y-5">
<input type="hidden" name="next" value={next} />
<label className="block text-sm font-medium text-slate-800">
E-mailadres
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<div className="space-y-2">
<Label htmlFor="email" className="text-slate-800">
E-mailadres
</Label>
<Input
id="email"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
type="email"
name="email"
autoComplete="email"
required
/>
</label>
</div>
<label className="block text-sm font-medium text-slate-800">
Wachtwoord
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<div className="space-y-2">
<Label htmlFor="password" className="text-slate-800">
Wachtwoord
</Label>
<Input
id="password"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
type="password"
name="password"
autoComplete="new-password"
minLength={8}
required
/>
</label>
</div>
<button
<Button
type="submit"
className="inline-flex w-full items-center justify-center rounded-2xl bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
size="lg"
className="w-full rounded-[1.25rem]"
>
Account aanmaken
</button>
</Button>
</form>
)}
</AuthPanel>

View file

@ -1,3 +1,6 @@
import { AlertCircleIcon, CheckCircle2Icon, InfoIcon } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { cn } from "@/lib/utils";
import { type AuthNotice } from "@/lib/auth/messages";
type AuthNoticeProps = {
@ -5,9 +8,19 @@ type AuthNoticeProps = {
};
const toneStyles = {
error: "border-rose-200 bg-rose-50 text-rose-900",
success: "border-emerald-200 bg-emerald-50 text-emerald-900",
info: "border-sky-200 bg-sky-50 text-sky-900",
error: {
className: "mb-5 border-rose-200 bg-rose-50 text-rose-950 [&_svg]:text-rose-700",
icon: AlertCircleIcon,
},
success: {
className:
"mb-5 border-emerald-200 bg-emerald-50 text-emerald-950 [&_svg]:text-emerald-700",
icon: CheckCircle2Icon,
},
info: {
className: "mb-5 border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700",
icon: InfoIcon,
},
};
export function AuthNotice({ notice }: AuthNoticeProps) {
@ -15,11 +28,15 @@ export function AuthNotice({ notice }: AuthNoticeProps) {
return null;
}
const tone = toneStyles[notice.tone];
const Icon = tone.icon;
return (
<div
className={`mb-5 rounded-2xl border px-4 py-3 text-sm leading-7 ${toneStyles[notice.tone]}`}
>
{notice.text}
</div>
<Alert className={cn("rounded-[1.5rem] px-4 py-3", tone.className)}>
<Icon className="size-4" />
<AlertDescription className="leading-7 text-current">
{notice.text}
</AlertDescription>
</Alert>
);
}

View file

@ -1,5 +1,9 @@
import Link from "next/link";
import type { ReactNode } from "react";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
type AuthPanelProps = {
eyebrow: string;
@ -43,20 +47,24 @@ export function AuthPanel({
</section>
<section className="flex items-center">
<div className="w-full rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<Card className="w-full rounded-[2rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur">
<CardContent className="p-6 sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<Link
href="/"
className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500 transition hover:text-slate-900"
className={cn(
buttonVariants({ variant: "ghost", size: "sm" }),
"h-auto p-0 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground hover:bg-transparent hover:text-foreground",
)}
>
Terug naar landing
</Link>
</div>
{children}
<div className="mt-6 border-t border-black/10 pt-5 text-sm text-slate-600">
{footer}
</div>
</div>
</div>
{children}
<Separator className="mt-6" />
<div className="pt-5 text-sm text-muted-foreground">{footer}</div>
</CardContent>
</Card>
</section>
</div>
</main>

View file

@ -3,6 +3,26 @@
import type { MouseEvent } from "react";
import { useState } from "react";
import { completeOnboardingAction, skipOnboardingAction } from "@/app/onboarding/actions";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import type { ProfileBundle } from "@/lib/profile/types";
@ -57,59 +77,59 @@ export function OnboardingFlow({ profileBundle }: OnboardingFlowProps) {
}
function goToNextStep(event: MouseEvent<HTMLButtonElement>) {
// This button lives inside the onboarding form. By preventing the default
// click action and rendering a keyed replacement, we avoid an accidental
// form submit when the final step button appears after the state update.
event.preventDefault();
setCurrentStep((stepIndex) => Math.min(steps.length - 1, stepIndex + 1));
}
return (
<div className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
<section className="rounded-[2rem] border border-black/10 bg-emerald-950 p-7 text-emerald-50 shadow-[0_18px_60px_rgba(6,78,59,0.18)] sm:p-9">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
<section className="rounded-[2rem] border border-primary/15 bg-primary p-7 text-primary-foreground shadow-[0_18px_60px_rgba(22,58,43,0.18)] sm:p-9">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/70">
{step.eyebrow}
</p>
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-tight sm:text-5xl">
{step.title}
</h1>
<p className="mt-5 max-w-xl text-base leading-8 text-emerald-50/85">
<p className="mt-5 max-w-xl text-base leading-8 text-primary-foreground/85">
{step.description}
</p>
<div className="mt-10 rounded-[1.5rem] border border-white/10 bg-white/8 p-5 text-sm leading-7 text-emerald-50/90">
<p className="font-semibold">Release 1 blijft bewust wellness-first.</p>
<ul className="mt-3 space-y-2">
<li>Alleen voor individuele gebruikers, zonder delen of zorgverlenerstoegang.</li>
<li>De app geeft geen diagnose, behandeling of medisch advies.</li>
<li>Bij acute of snel verslechterende klachten hoort directe hulp via arts, huisartsenpost of 112 buiten deze app.</li>
</ul>
</div>
<Alert className="mt-10 rounded-[1.5rem] border-white/10 bg-white/8 text-primary-foreground [&_svg]:text-primary-foreground/80">
<AlertDescription className="leading-7 text-current">
<span className="block font-semibold">Release 1 blijft bewust wellness-first.</span>
<span className="mt-2 block">
Alleen voor individuele gebruikers, zonder delen of zorgverlenerstoegang.
</span>
<span className="block">
De app geeft geen diagnose, behandeling of medisch advies.
</span>
<span className="block">
Bij acute of snel verslechterende klachten hoort directe hulp via arts, huisartsenpost of 112 buiten deze app.
</span>
</AlertDescription>
</Alert>
<ol className="mt-8 flex gap-3">
{steps.map((item, index) => (
<li
key={item.title}
className={`h-2 flex-1 rounded-full ${
index <= currentStep ? "bg-emerald-200" : "bg-white/15"
index <= currentStep ? "bg-primary-foreground/85" : "bg-white/15"
}`}
/>
))}
</ol>
</section>
<section className="rounded-[2rem] border border-black/10 bg-white/75 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<section className="rounded-[2rem] border border-border/60 bg-card/90 p-6 shadow-[0_18px_60px_rgba(71,85,105,0.12)] backdrop-blur sm:p-8">
<div className="mb-6 flex items-center justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Korte onboarding
</p>
<form action={skipOnboardingAction}>
<button
type="submit"
className="rounded-full border border-black/10 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950"
>
<Button type="submit" variant="outline" className="rounded-full">
Nu overslaan
</button>
</Button>
</form>
</div>
@ -135,154 +155,184 @@ export function OnboardingFlow({ profileBundle }: OnboardingFlowProps) {
{currentStep === 0 ? (
<div className="space-y-4">
<article className="rounded-[1.5rem] border border-black/10 bg-stone-50 p-5">
<h2 className="font-[family-name:var(--font-display)] text-2xl text-slate-900">
Wat je hier wél krijgt
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Een rustige plan-doe-evalueer flow met energiebudgetten, zonder
druk, score-oordeel of medische terminologie.
</p>
</article>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<CardHeader className="pb-0">
<CardTitle className="font-[family-name:var(--font-display)] text-2xl">
Wat je hier wél krijgt
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-sm leading-7 text-muted-foreground">
Een rustige plan-doe-evalueer flow met energiebudgetten, zonder
druk, score-oordeel of medische terminologie.
</CardDescription>
</CardContent>
</Card>
<article className="rounded-[1.5rem] border border-black/10 bg-stone-50 p-5">
<h2 className="font-[family-name:var(--font-display)] text-2xl text-slate-900">
Wat deze app niet doet
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Geen diagnose, geen behandeling, geen medische triage en geen
automatisch delen met derden.
</p>
</article>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<CardHeader className="pb-0">
<CardTitle className="font-[family-name:var(--font-display)] text-2xl">
Wat deze app niet doet
</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-sm leading-7 text-muted-foreground">
Geen diagnose, geen behandeling, geen medische triage en geen
automatisch delen met derden.
</CardDescription>
</CardContent>
</Card>
</div>
) : null}
{currentStep === 1 ? (
<div className="space-y-5">
<label className="block text-sm font-medium text-slate-800">
Schermnaam
<input
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<div className="space-y-2">
<Label htmlFor="display-name" className="text-slate-800">
Schermnaam
</Label>
<Input
id="display-name"
className="h-12 rounded-[1.25rem] bg-background/80 px-4 text-base md:text-base"
type="text"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
placeholder="Optioneel, bijvoorbeeld Jan"
maxLength={40}
/>
</label>
<div className="rounded-[1.5rem] border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Voertaal voor release 1 staat vast op <strong>Nederlands</strong>.
</div>
<label className="block text-sm font-medium text-slate-800">
Timezone
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<Alert className="rounded-[1.5rem] border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700">
<AlertDescription className="leading-7 text-current">
Voertaal voor release 1 staat vast op <strong>Nederlands</strong>.
</AlertDescription>
</Alert>
<div className="space-y-2">
<Label className="text-slate-800">Timezone</Label>
<Select
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
onValueChange={(value) =>
setTimezone(value ?? profileBundle.profile.timezone)
}
>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
<SelectValue placeholder="Kies een timezone" />
</SelectTrigger>
<SelectContent>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
) : null}
{currentStep === 2 ? (
<div className="space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={showEnergyPoints}
onChange={(event) => setShowEnergyPoints(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Laat geplande en resterende punten zichtbaar zien in de interface.
</span>
</span>
</label>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Laat geplande en resterende punten zichtbaar zien in de interface.
</p>
</div>
<Switch checked={showEnergyPoints} onCheckedChange={setShowEnergyPoints} />
</CardContent>
</Card>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={morningReminderEnabled}
onChange={(event) => setMorningReminderEnabled(event.target.checked)}
/>
<span className="flex-1">
<span className="block text-sm font-semibold text-slate-900">
Zet een lichte ochtendreminder aan
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Handig als je later een korte check-in wilt doen zonder extra druk.
</span>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<CardContent className="space-y-4 py-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
Zet een lichte ochtendreminder aan
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Handig als je later een korte check-in wilt doen zonder extra druk.
</p>
</div>
<Switch
checked={morningReminderEnabled}
onCheckedChange={setMorningReminderEnabled}
/>
</div>
{morningReminderEnabled ? (
<input
className="mt-3 w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-base outline-none transition focus:border-emerald-600"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
<>
<Separator />
<div className="space-y-2">
<Label htmlFor="morning-reminder-time" className="text-slate-800">
Tijdstip voor de ochtendreminder
</Label>
<Input
id="morning-reminder-time"
className="h-12 rounded-[1.25rem] bg-white px-4 text-base md:text-base"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
</div>
</>
) : null}
</span>
</label>
</CardContent>
</Card>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={reflectionReminderEnabled}
onChange={(event) => setReflectionReminderEnabled(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Sta lichte reflectieprompts toe
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Optionele terugblikprompts kunnen later helpen om rustiger patronen te zien.
</span>
</span>
</label>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label className="text-sm font-semibold text-slate-900">
Sta lichte reflectieprompts toe
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Optionele terugblikprompts kunnen later helpen om rustiger patronen te zien.
</p>
</div>
<Switch
checked={reflectionReminderEnabled}
onCheckedChange={setReflectionReminderEnabled}
/>
</CardContent>
</Card>
</div>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
<button
<Separator />
<div className="flex flex-wrap items-center justify-between gap-3">
<Button
type="button"
variant="outline"
onClick={goToPreviousStep}
disabled={isFirstStep}
className="rounded-full border border-black/10 bg-white px-5 py-3 text-sm font-medium text-slate-700 transition hover:-translate-y-0.5 hover:text-slate-950 disabled:cursor-not-allowed disabled:opacity-45"
className="rounded-full"
>
Vorige
</button>
</Button>
{isLastStep ? (
<button
<Button
key="complete-onboarding"
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
className="rounded-full"
>
Rond onboarding af
</button>
</Button>
) : (
<button
<Button
key={`next-step-${currentStep}`}
type="button"
onClick={goToNextStep}
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
className="rounded-full"
>
Ga verder
</button>
</Button>
)}
</div>
</form>

View file

@ -2,6 +2,26 @@
import { useState } from "react";
import { saveSettingsAction } from "@/app/settings/actions";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
import type { ProfileBundle } from "@/lib/profile/types";
@ -53,164 +73,197 @@ export function SettingsForm({ profileBundle }: SettingsFormProps) {
value={reflectionReminderEnabled ? "true" : "false"}
/>
<section className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Account
</p>
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-slate-900">
Basisinstellingen voor jouw account
</h2>
<p className="mt-3 max-w-2xl text-sm leading-7 text-slate-700">
Je past hier alleen je wellness-first voorkeuren aan. Er zijn in release 1
geen medische velden, deelinstellingen of zorgverlenerrollen.
</p>
</section>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_18px_60px_rgba(71,85,105,0.1)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Account
</p>
<CardTitle className="font-[family-name:var(--font-display)] text-3xl text-slate-900">
Basisinstellingen voor jouw account
</CardTitle>
<CardDescription className="max-w-2xl text-sm leading-7 text-muted-foreground">
Je past hier alleen wellness-first voorkeuren aan. Er zijn in release 1
geen medische velden, deelinstellingen of zorgverlenerrollen.
</CardDescription>
</CardHeader>
<CardContent className="pt-1 pb-6">
<Alert className="rounded-[1.5rem] border-sky-200 bg-sky-50 text-sky-950 [&_svg]:text-sky-700">
<AlertDescription className="leading-7 text-current">
Release 1 draait bewust volledig in het <strong>Nederlands</strong>.
De taalinstelling blijft wel al aanwezig in het accountmodel.
</AlertDescription>
</Alert>
</CardContent>
</Card>
<section className="grid gap-5 lg:grid-cols-2">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Taal en tijd
</p>
<div className="mt-5 space-y-5">
<label className="block text-sm font-medium text-slate-800">
Taal
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Taal en tijd
</p>
</CardHeader>
<CardContent className="space-y-5 pb-6">
<div className="space-y-2">
<Label className="text-slate-800">Taal</Label>
<Select
value={locale}
onChange={(event) => setLocale(event.target.value)}
onValueChange={(value) => setLocale(value ?? profileBundle.profile.locale)}
>
{LOCALE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<div className="rounded-[1.5rem] border border-sky-200 bg-sky-50 px-4 py-4 text-sm leading-7 text-sky-900">
Release 1 draait bewust volledig in het Nederlands. De taalinstelling
blijft al wel aanwezig in het accountmodel.
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
<SelectValue placeholder="Kies een taal" />
</SelectTrigger>
<SelectContent>
{LOCALE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<label className="block text-sm font-medium text-slate-800">
Timezone
<select
className="mt-2 w-full rounded-2xl border border-black/10 bg-stone-50 px-4 py-3 text-base outline-none transition focus:border-emerald-600 focus:bg-white"
<div className="space-y-2">
<Label className="text-slate-800">Timezone</Label>
<Select
value={timezone}
onChange={(event) => setTimezone(event.target.value)}
onValueChange={(value) =>
setTimezone(value ?? profileBundle.profile.timezone)
}
>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
</article>
<SelectTrigger className="h-12 w-full rounded-[1.25rem] bg-background/80 px-4 text-base">
<SelectValue placeholder="Kies een timezone" />
</SelectTrigger>
<SelectContent>
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Interface
</p>
<div className="mt-5 space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={showEnergyPoints}
onChange={(event) => setShowEnergyPoints(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Laat budgetpunten zichtbaar zien in het dashboard en latere dagflows.
</span>
</span>
</label>
</div>
</article>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Interface
</p>
</CardHeader>
<CardContent className="space-y-4 pb-6">
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label htmlFor="show-energy-points" className="text-sm font-semibold text-slate-900">
Toon energiebudgetpunten
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Laat budgetpunten zichtbaar zien in het dashboard en latere dagflows.
</p>
</div>
<Switch
id="show-energy-points"
checked={showEnergyPoints}
onCheckedChange={setShowEnergyPoints}
/>
</CardContent>
</Card>
</CardContent>
</Card>
</section>
<section className="grid gap-5 lg:grid-cols-2">
<article className="rounded-[1.75rem] border border-black/10 bg-white/75 p-6 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
Reminders
</p>
<div className="mt-5 space-y-4">
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={morningReminderEnabled}
onChange={(event) => setMorningReminderEnabled(event.target.checked)}
/>
<span className="flex-1">
<span className="block text-sm font-semibold text-slate-900">
Ochtendreminder
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Zet een lichte reminder aan voor een rustige start van je check-in.
</span>
<Card className="rounded-[1.75rem] border border-border/60 bg-card/90 py-0 shadow-[0_12px_40px_rgba(71,85,105,0.08)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground">
Reminders
</p>
</CardHeader>
<CardContent className="space-y-4 pb-6">
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<CardContent className="space-y-4 py-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="morning-reminder-enabled" className="text-sm font-semibold text-slate-900">
Ochtendreminder
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Zet een lichte reminder aan voor een rustige start van je check-in.
</p>
</div>
<Switch
id="morning-reminder-enabled"
checked={morningReminderEnabled}
onCheckedChange={setMorningReminderEnabled}
/>
</div>
{morningReminderEnabled ? (
<input
className="mt-3 w-full rounded-2xl border border-black/10 bg-white px-4 py-3 text-base outline-none transition focus:border-emerald-600"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
<>
<Separator />
<div className="space-y-2">
<Label htmlFor="morning-reminder-time" className="text-slate-800">
Tijdstip voor de ochtendreminder
</Label>
<Input
id="morning-reminder-time"
className="h-12 rounded-[1.25rem] bg-white px-4 text-base md:text-base"
type="time"
value={morningReminderTime}
onChange={(event) => setMorningReminderTime(event.target.value)}
/>
</div>
</>
) : null}
</span>
</label>
</CardContent>
</Card>
<label className="flex items-start gap-3 rounded-[1.5rem] border border-black/10 bg-stone-50 px-4 py-4">
<input
className="mt-1 h-4 w-4 accent-emerald-900"
type="checkbox"
checked={reflectionReminderEnabled}
onChange={(event) => setReflectionReminderEnabled(event.target.checked)}
/>
<span>
<span className="block text-sm font-semibold text-slate-900">
Reflectieprompts toestaan
</span>
<span className="mt-1 block text-sm leading-7 text-slate-700">
Maak alvast de opt-in klaar voor lichte terugblikprompts in een latere story.
</span>
</span>
</label>
</div>
</article>
<Card className="rounded-[1.5rem] border border-border/60 bg-background/80 py-0 shadow-none">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<Label htmlFor="reflection-reminder-enabled" className="text-sm font-semibold text-slate-900">
Reflectieprompts toestaan
</Label>
<p className="text-sm leading-7 text-muted-foreground">
Maak alvast de opt-in klaar voor lichte terugblikprompts in een latere story.
</p>
</div>
<Switch
id="reflection-reminder-enabled"
checked={reflectionReminderEnabled}
onCheckedChange={setReflectionReminderEnabled}
/>
</CardContent>
</Card>
</CardContent>
</Card>
<article className="rounded-[1.75rem] border border-emerald-950/10 bg-emerald-950 p-6 text-emerald-50 shadow-[0_12px_40px_rgba(6,78,59,0.18)]">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
Bewuste grenzen
</p>
<ul className="mt-4 space-y-3 text-sm leading-7 text-emerald-50/90">
<li>Geen medische drempels, diagnoses of behandelinstellingen.</li>
<li>Geen delen met zorgverleners of naasten in release 1.</li>
<li>Alle instellingen blijven gekoppeld aan alleen jouw account.</li>
</ul>
</article>
<Card className="rounded-[1.75rem] border border-primary/15 bg-primary py-0 text-primary-foreground shadow-[0_12px_40px_rgba(22,58,43,0.18)]">
<CardHeader className="pb-0">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-primary-foreground/75">
Bewuste grenzen
</p>
</CardHeader>
<CardContent className="space-y-3 pb-6 text-sm leading-7 text-primary-foreground/90">
<p>Geen medische drempels, diagnoses of behandelinstellingen.</p>
<p>Geen delen met zorgverleners of naasten in release 1.</p>
<p>Alle instellingen blijven gekoppeld aan alleen jouw account.</p>
</CardContent>
</Card>
</section>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
<p className="text-sm leading-7 text-slate-600">
<Separator />
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm leading-7 text-muted-foreground">
Wijzigingen zijn direct van toepassing op jouw account en volgende sessies.
</p>
<button
type="submit"
className="rounded-full bg-emerald-950 px-5 py-3 text-sm font-semibold text-emerald-50 transition hover:-translate-y-0.5 hover:bg-emerald-900"
>
<Button type="submit" size="lg" className="h-11 rounded-full px-5">
Instellingen opslaan
</button>
</Button>
</div>
</form>
);

76
components/ui/alert.tsx Normal file
View file

@ -0,0 +1,76 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className
)}
{...props}
/>
)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-action"
className={cn("absolute top-2 right-2", className)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription, AlertAction }

103
components/ui/card.tsx Normal file
View file

@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -0,0 +1,29 @@
"use client"
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { cn } from "@/lib/utils"
import { CheckIcon } from "lucide-react"
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
<CheckIcon
/>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

20
components/ui/input.tsx Normal file
View file

@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

20
components/ui/label.tsx Normal file
View file

@ -0,0 +1,20 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

201
components/ui/select.tsx Normal file
View file

@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
const Select = SelectPrimitive.Root
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
}
/>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View file

@ -0,0 +1,25 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

32
components/ui/switch.tsx Normal file
View file

@ -0,0 +1,32 @@
"use client"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: SwitchPrimitive.Root.Props & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View file

@ -32,11 +32,13 @@ Deze map bevat de vernieuwde documentatie voor de gekozen `wellness/self-managem
- Hosting: `Vercel`
- Database: `Supabase PostgreSQL`
- Authenticatie: `Supabase Auth`
- UI foundation in de app: `Tailwind CSS + shadcn/ui`
## Generator
- [generate_inspannings_monitor_docs.py](./generate_inspannings_monitor_docs.py)
Genereert de actuele `.docx`-documenten opnieuw vanuit de bevestigde uitgangspunten.
Vereist een Python-omgeving met `python-docx`.
## Backlog en Linear

View file

@ -755,7 +755,7 @@ def build_technische_architectuur() -> None:
[
["Framework", "Next.js met App Router", "Past goed bij Vercel, server components en server-side datatoegang."],
["UI", "React + TypeScript", "Sterke typeveiligheid en goede componentstructuur."],
["Styling", "Tailwind CSS + componentbibliotheek", "Snel consistente, toegankelijke UI voor mobile-first flows."],
["Styling", "Tailwind CSS + shadcn/ui", "Semantische componenten en centrale theme tokens houden de UI consistenter en beter onderhoudbaar."],
["Validatie", "Zod of vergelijkbare runtime-validatie", "Nodig voor strikte server-side inputcontrole."],
["Database access", "Supabase client en/of Prisma", "Prisma kan nog steeds zinvol zijn, maar is een bewuste keuze en geen verplichting."],
["Charts", "Lichte grafiekbibliotheek", "Alleen voor eenvoudige dag- en weekinzichten; geen heavy analytics platform nodig."],
@ -1037,7 +1037,7 @@ def build_implementatieplan_backlog() -> None:
[
["ST-001", "Next.js projectbasis opzetten met TypeScript en gekozen stylingaanpak.", "Build", "Project start lokaal en in previewomgeving zonder handmatige workarounds."],
["ST-002", "Omgevingen definiëren voor development, preview en production.", "Ops", "Environment strategy is vastgelegd en werkt technisch."],
["ST-003", "Component foundation voor formulieren, kaarten, knoppen en meldingen neerzetten.", "UI", "Kerncomponenten zijn herbruikbaar en mobiel bruikbaar."],
["ST-003", "shadcn/ui foundation voor formulieren, kaarten, knoppen en meldingen neerzetten.", "UI", "Kerncomponenten zijn herbruikbaar, thematisch consistent en mobiel bruikbaar."],
["ST-004", "Basale foutafhandeling en lege staten ontwerpen.", "UX", "Gebruiker ziet bruikbare feedback bij lege of foutieve situaties."],
],
)