feat: initial commit
This commit is contained in:
commit
7d443a004a
76 changed files with 15704 additions and 0 deletions
25
components/auth/auth-notice.tsx
Normal file
25
components/auth/auth-notice.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { type AuthNotice } from "@/lib/auth/messages";
|
||||
|
||||
type AuthNoticeProps = {
|
||||
notice: AuthNotice | null;
|
||||
};
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
export function AuthNotice({ notice }: AuthNoticeProps) {
|
||||
if (!notice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`mb-5 rounded-2xl border px-4 py-3 text-sm leading-7 ${toneStyles[notice.tone]}`}
|
||||
>
|
||||
{notice.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
components/auth/auth-panel.tsx
Normal file
64
components/auth/auth-panel.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type AuthPanelProps = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
children: ReactNode;
|
||||
footer: ReactNode;
|
||||
};
|
||||
|
||||
export function AuthPanel({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
footer,
|
||||
}: AuthPanelProps) {
|
||||
return (
|
||||
<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 grid min-h-[calc(100vh-5rem)] max-w-6xl gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<section className="flex flex-col justify-between 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">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-emerald-200/80">
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h1 className="mt-4 font-[family-name:var(--font-display)] text-4xl leading-tight sm:text-5xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-base leading-8 text-emerald-50/85">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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 licht.</p>
|
||||
<ul className="mt-3 space-y-2">
|
||||
<li>Wellness-first en alleen voor individuele gebruikers</li>
|
||||
<li>Geen zorgverlenerstoegang, sharing of AI in deze fase</li>
|
||||
<li>Authenticatie via Supabase met cookie-based sessies</li>
|
||||
</ul>
|
||||
</div>
|
||||
</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">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500 transition hover:text-slate-900"
|
||||
>
|
||||
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>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
292
components/onboarding/onboarding-flow.tsx
Normal file
292
components/onboarding/onboarding-flow.tsx
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
"use client";
|
||||
|
||||
import type { MouseEvent } from "react";
|
||||
import { useState } from "react";
|
||||
import { completeOnboardingAction, skipOnboardingAction } from "@/app/onboarding/actions";
|
||||
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
|
||||
import type { ProfileBundle } from "@/lib/profile/types";
|
||||
|
||||
type OnboardingFlowProps = {
|
||||
profileBundle: ProfileBundle;
|
||||
};
|
||||
|
||||
const steps = [
|
||||
{
|
||||
eyebrow: "Stap 1",
|
||||
title: "Zo gebruiken we Inspannings Monitor",
|
||||
description:
|
||||
"De app helpt je om je dag rustiger te plannen en terug te kijken zonder medische claims of zorgverlenerfuncties.",
|
||||
},
|
||||
{
|
||||
eyebrow: "Stap 2",
|
||||
title: "Basisprofiel",
|
||||
description:
|
||||
"Kies hoe de app je mag aanspreken en welke timezone het best bij je dagindeling past.",
|
||||
},
|
||||
{
|
||||
eyebrow: "Stap 3",
|
||||
title: "Startvoorkeuren",
|
||||
description:
|
||||
"Kies rustig hoe zichtbaar je energiebudget is en of je lichte reminders wilt ontvangen.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function OnboardingFlow({ profileBundle }: OnboardingFlowProps) {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [displayName, setDisplayName] = useState(profileBundle.profile.displayName ?? "");
|
||||
const [timezone, setTimezone] = useState(profileBundle.profile.timezone);
|
||||
const [showEnergyPoints, setShowEnergyPoints] = useState(
|
||||
profileBundle.settings.showEnergyPoints,
|
||||
);
|
||||
const [morningReminderEnabled, setMorningReminderEnabled] = useState(
|
||||
profileBundle.settings.morningReminderEnabled,
|
||||
);
|
||||
const [morningReminderTime, setMorningReminderTime] = useState(
|
||||
profileBundle.settings.morningReminderTime ?? "08:30",
|
||||
);
|
||||
const [reflectionReminderEnabled, setReflectionReminderEnabled] = useState(
|
||||
profileBundle.settings.reflectionReminderEnabled,
|
||||
);
|
||||
|
||||
const step = steps[currentStep];
|
||||
const isFirstStep = currentStep === 0;
|
||||
const isLastStep = currentStep === steps.length - 1;
|
||||
|
||||
function goToPreviousStep() {
|
||||
setCurrentStep((stepIndex) => Math.max(0, stepIndex - 1));
|
||||
}
|
||||
|
||||
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">
|
||||
{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">
|
||||
{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>
|
||||
|
||||
<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"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</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">
|
||||
<div className="mb-6 flex items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-500">
|
||||
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"
|
||||
>
|
||||
Nu overslaan
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form action={completeOnboardingAction} className="space-y-6">
|
||||
<input type="hidden" name="displayName" value={displayName} />
|
||||
<input type="hidden" name="timezone" value={timezone} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="showEnergyPoints"
|
||||
value={showEnergyPoints ? "true" : "false"}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="morningReminderEnabled"
|
||||
value={morningReminderEnabled ? "true" : "false"}
|
||||
/>
|
||||
<input type="hidden" name="morningReminderTime" value={morningReminderTime} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="reflectionReminderEnabled"
|
||||
value={reflectionReminderEnabled ? "true" : "false"}
|
||||
/>
|
||||
|
||||
{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>
|
||||
|
||||
<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>
|
||||
</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"
|
||||
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"
|
||||
value={timezone}
|
||||
onChange={(event) => setTimezone(event.target.value)}
|
||||
>
|
||||
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
{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)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/10 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Vorige
|
||||
</button>
|
||||
|
||||
{isLastStep ? (
|
||||
<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"
|
||||
>
|
||||
Rond onboarding af
|
||||
</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"
|
||||
>
|
||||
Ga verder
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
217
components/settings/settings-form.tsx
Normal file
217
components/settings/settings-form.tsx
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { saveSettingsAction } from "@/app/settings/actions";
|
||||
import { ONBOARDING_TIMEZONE_OPTIONS } from "@/lib/onboarding/options";
|
||||
import type { ProfileBundle } from "@/lib/profile/types";
|
||||
|
||||
type SettingsFormProps = {
|
||||
profileBundle: ProfileBundle;
|
||||
};
|
||||
|
||||
const LOCALE_OPTIONS = [
|
||||
{
|
||||
value: "nl-NL",
|
||||
label: "Nederlands",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function SettingsForm({ profileBundle }: SettingsFormProps) {
|
||||
const [locale, setLocale] = useState(profileBundle.profile.locale);
|
||||
const [timezone, setTimezone] = useState(profileBundle.profile.timezone);
|
||||
const [showEnergyPoints, setShowEnergyPoints] = useState(
|
||||
profileBundle.settings.showEnergyPoints,
|
||||
);
|
||||
const [morningReminderEnabled, setMorningReminderEnabled] = useState(
|
||||
profileBundle.settings.morningReminderEnabled,
|
||||
);
|
||||
const [morningReminderTime, setMorningReminderTime] = useState(
|
||||
profileBundle.settings.morningReminderTime ?? "08:30",
|
||||
);
|
||||
const [reflectionReminderEnabled, setReflectionReminderEnabled] = useState(
|
||||
profileBundle.settings.reflectionReminderEnabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={saveSettingsAction} className="space-y-6">
|
||||
<input type="hidden" name="locale" value={locale} />
|
||||
<input type="hidden" name="timezone" value={timezone} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="showEnergyPoints"
|
||||
value={showEnergyPoints ? "true" : "false"}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="morningReminderEnabled"
|
||||
value={morningReminderEnabled ? "true" : "false"}
|
||||
/>
|
||||
<input type="hidden" name="morningReminderTime" value={morningReminderTime} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="reflectionReminderEnabled"
|
||||
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>
|
||||
|
||||
<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"
|
||||
value={locale}
|
||||
onChange={(event) => setLocale(event.target.value)}
|
||||
>
|
||||
{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.
|
||||
</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"
|
||||
value={timezone}
|
||||
onChange={(event) => setTimezone(event.target.value)}
|
||||
>
|
||||
{ONBOARDING_TIMEZONE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{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)}
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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">
|
||||
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"
|
||||
>
|
||||
Instellingen opslaan
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
58
components/ui/button.tsx
Normal file
58
components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
Loading…
Add table
Add a link
Reference in a new issue