diff --git a/app/page.tsx b/app/page.tsx index ca3f013..7b9ceb9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -64,12 +64,22 @@ export default async function Home({ searchParams }: HomePageProps) { title="Over de maker en de app" description="Inspannings Monitor is een rustige wellness-first webapp voor volwassenen die hun energie willen plannen, uitvoeren en evalueren zonder medische claims of overmatige frictie." aside={ - - Bekijk planning - +
+ + Curriculum vitae + + + Specificatie + +
} /> diff --git a/app/specificatie/page.tsx b/app/specificatie/page.tsx new file mode 100644 index 0000000..81005d1 --- /dev/null +++ b/app/specificatie/page.tsx @@ -0,0 +1,138 @@ +import { redirect } from "next/navigation"; +import { getAuthState } from "@/lib/auth/session"; +import { AppShell } from "@/components/navigation/app-shell"; +import { PageIntro } from "@/components/navigation/page-intro"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +export const dynamic = "force-dynamic"; + +const chapters = [ + { + number: "01", + title: "Productkader en positionering", + version: "v06", + summary: + "Legt vast wat Inspannings Monitor wel en niet is. De app positioneert zich expliciet als wellness/self-management tool, zonder medische claims of zorgverlenerrollen. De intended use is energieplanning en dagstructuur voor volwassenen. Non-intended use — zoals diagnostiek, behandeling of klinische besluitvorming — is bewust uitgesloten en wordt bewaakt via claim-guardrails in tekst, UI en onboarding.", + points: [ + "Productnaam: Inspannings Monitor", + "Positionering: wellness / self-management", + "Doelgroep: volwassen individuele gebruikers", + "Voertaal release 1: Nederlands (nl-NL)", + "Geen medische claims, geen zorgverlenerrollen", + "Expliciete non-intended use vastgelegd als guardrail", + ], + }, + { + number: "02", + title: "Functionele specificatie MVP", + version: "v06", + summary: + "Beschrijft de MVP in toetsbare functionele requirements. De kern is een plan-doe-evalueer-dagflow: ochtendcheck-in met energiescore en slaapkwaliteit, activiteitenplanning met een licht energiebudget, en een dagafsluiting. Elke stap is bewust klein gehouden om de cognitieve belasting laag te houden. Release 1 bevat geen AI, geen deelfunctionaliteit en geen medische workflows.", + points: [ + "Ochtendcheck-in: energiescore (1–10) en slaapkwaliteit", + "Dagbudget afgeleid van energiescore", + "Activiteitenplanning met duur, impact en prioriteit", + "Statussen: gepland → uitgevoerd / aangepast / overgeslagen", + "Dashboard met dag-overzicht en energiemeter", + "Geen sharing, AI of medische workflows in de MVP", + ], + }, + { + number: "03", + title: "Privacy, security en safety baseline", + version: "v02", + summary: + "Bundelt de minimale randvoorwaarden voor privacy, informatiebeveiliging en productveiligheid. Gebruikersdata blijft strikt gescheiden via Row Level Security in Supabase. Authenticatie vereist e-mailverificatie. Gevoelige sleutels worden nooit in de frontend geplaatst. De baseline is ontworpen als minimumset die uitbreidbaar is als het product richting een medisch spoor beweegt.", + points: [ + "Row Level Security: gebruikers zien alleen eigen data", + "Supabase Auth met verplichte e-mailverificatie", + "Service-role key alleen server-side, nooit in de frontend", + "HTTPS-only via Vercel, geen gevoelige data in URL-params", + "Geen opslag van medische of klinische gegevens", + "Baseline uitbreidbaar richting NEN 7510 / MDR bij medisch spoor", + ], + }, + { + number: "04", + title: "Roadmap wellness naar medisch", + version: "v02", + summary: + "Laat zien hoe Inspannings Monitor gecontroleerd kan doorgroeien naar een medisch product, zonder dat dit ongemerkt in de wellness-release binnensluipt. De roadmap onderscheidt drie fasen: verankerde wellness-MVP, optionele zorgverlenerlaag en een apart medisch productspoor. Elke fase kent eigen vereisten voor regelgeving, documentatie en technische architectuur.", + points: [ + "Fase 1: wellness MVP — volledig buiten MDR-scope", + "Fase 2: optionele zorgverlenerlaag met expliciete scope-bewaking", + "Fase 3: apart medisch productspoor met eigen regulatoir traject", + "Faseovergangen vereisen bewuste go/no-go beslissing", + "Geen sluipende scope-uitbreiding zonder documentatieverandering", + "Pad opengehouden zonder verplichting om het te bewandelen", + ], + }, + { + number: "05", + title: "Technische architectuur en implementatie", + version: "v01", + summary: + "Beschrijft de technische implementatielaag van de wellness-first MVP als zelfstandig architectuurdocument. De stack is bewust compact gehouden: Next.js App Router met Server Actions, Supabase voor auth en data, en Vercel voor hosting en CI/CD. De architectuur is opgezet met duidelijke laagscheiding zodat uitbreiding naar een medisch spoor later geïsoleerd kan plaatsvinden.", + points: [ + "Stack: Next.js 16 (App Router) + React 19 + TypeScript", + "Database en auth: Supabase (PostgreSQL + Row Level Security)", + "UI: shadcn/ui + Tailwind CSS, Dusk dark-mode thema", + "Hosting en deploys: Vercel met GitHub Actions CI", + "Server Actions voor alle formmutaties, geen aparte API-laag", + "Migraties via supabase/migrations, seeding via npm-script", + ], + }, +]; + +export default async function SpecificatiePage() { + const authState = await getAuthState(); + + if (!authState.isAuthenticated) { + redirect("/login"); + } + + return ( + + + +
+ {chapters.map((chapter) => ( + + +

+ Hoofdstuk {chapter.number} · {chapter.version} +

+ + {chapter.title} + + + {chapter.summary} + +
+ +
+ {chapter.points.map((point) => ( + + + {point} + + + ))} +
+
+
+ ))} +
+
+ ); +} diff --git a/components/navigation/account-menu.tsx b/components/navigation/account-menu.tsx index dcf7619..2dac116 100644 --- a/components/navigation/account-menu.tsx +++ b/components/navigation/account-menu.tsx @@ -11,6 +11,14 @@ import { import { signOutAction } from "@/app/auth-actions"; import type { AuthState } from "@/lib/auth/session"; import { ProfileAvatar } from "@/components/profile/profile-avatar"; +import type { NavProfile } from "@/lib/profile/service"; + +function formatNavDisplayName(displayName: string | null): string | null { + if (!displayName) return null; + const parts = displayName.trim().split(/\s+/); + if (parts.length < 2) return displayName; + return `${parts[0][0]}. ${parts[parts.length - 1]}`; +} import { DropdownMenu, DropdownMenuContent, @@ -22,26 +30,27 @@ import { type AccountMenuProps = { authState: AuthState; - navAvatarUrl: string | null; + navProfile: NavProfile | null; }; -export function AccountMenu({ authState, navAvatarUrl }: AccountMenuProps) { - const showAvatar = authState.isAuthenticated && navAvatarUrl; +export function AccountMenu({ authState, navProfile }: AccountMenuProps) { + const showAvatar = authState.isAuthenticated && navProfile?.avatarUrl; + const navLabel = formatNavDisplayName(navProfile?.displayName ?? null) ?? "Account"; return ( {showAvatar ? ( ) : ( )} - Account + {navLabel} {authState.isConfigured ? ( diff --git a/components/navigation/app-shell.tsx b/components/navigation/app-shell.tsx index 591934e..782da34 100644 --- a/components/navigation/app-shell.tsx +++ b/components/navigation/app-shell.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; import { getAuthState } from "@/lib/auth/session"; -import { getNavAvatarUrlForCurrentUser } from "@/lib/profile/service"; +import { getNavProfileForCurrentUser } from "@/lib/profile/service"; import { BottomNav } from "@/components/navigation/bottom-nav"; import { TopNav } from "@/components/navigation/top-nav"; import { cn } from "@/lib/utils"; @@ -15,14 +15,14 @@ export async function AppShell({ contentClassName, }: AppShellProps) { const authState = await getAuthState(); - const navAvatarUrl = authState.userId - ? await getNavAvatarUrlForCurrentUser(authState.userId) + const navProfile = authState.userId + ? await getNavProfileForCurrentUser(authState.userId) : null; return (
- +
{children}
diff --git a/components/navigation/top-nav.tsx b/components/navigation/top-nav.tsx index 85796a8..6d2af00 100644 --- a/components/navigation/top-nav.tsx +++ b/components/navigation/top-nav.tsx @@ -1,8 +1,10 @@ "use client"; +import Image from "next/image"; import Link from "next/link"; import { usePathname } from "next/navigation"; import type { AuthState } from "@/lib/auth/session"; +import type { NavProfile } from "@/lib/profile/service"; import { AccountMenu } from "@/components/navigation/account-menu"; import { isActivePath, @@ -14,23 +16,32 @@ import { cn } from "@/lib/utils"; type TopNavProps = { authState: AuthState; - navAvatarUrl: string | null; + navProfile: NavProfile | null; }; -export function TopNav({ authState, navAvatarUrl }: TopNavProps) { +export function TopNav({ authState, navProfile }: TopNavProps) { const pathname = usePathname(); const useCompactBottomNav = shouldUseBottomNav(pathname); return (
- - - Inspannings Monitor - - - Wellness-first dagflow - + + Inspannings Monitor icoon +
+ + Inspannings Monitor + + + Wellness-first dagflow + +
diff --git a/components/profile/profile-avatar.tsx b/components/profile/profile-avatar.tsx index 239ddc8..517be0a 100644 --- a/components/profile/profile-avatar.tsx +++ b/components/profile/profile-avatar.tsx @@ -52,7 +52,7 @@ export function ProfileAvatar({
{ +export type NavProfile = { + avatarUrl: string | null; + displayName: string | null; +}; + +export async function getNavProfileForCurrentUser(userId: string): Promise { const supabase = await createClient(); const { data } = await supabase .from("profiles") - .select("avatar_path") + .select("avatar_path, display_name") .eq("id", userId) .maybeSingle(); - return getProfileAvatarUrl(data?.avatar_path ?? null); + return { + avatarUrl: await getProfileAvatarUrl(data?.avatar_path ?? null), + displayName: data?.display_name ?? null, + }; } + diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/package.json b/package.json index 36861a6..cda1050 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inspannings-monitor", - "version": "0.1.0", + "version": "0.1.1", "private": true, "scripts": { "dev": "next dev",