Frontend
De frontend wordt vanaf 0 opgebouwd als een monorepo met React 19, Vite en TypeScript, getoetst aan het domeinmodel als spec. De prototype-frontend (React in JavaScript) dient uitsluitend als referentie/uitgangspunt, niet als basis: greenfield betekent dat we strikt getypeerd, modulair en testbaar herbouwen.
Er zijn twee portalen — een beheerportaal en een gebruikersportaal — die samen tegen één backend-API praten. De scheiding tussen beide loopt via autorisatiebeleid (beheerder versus gebruiker/anoniem), niet via een aparte API of een 1-op-1-grens met een bounded context. Grofweg bedient het beheerportaal de context Beheer plus de admin-kant van Moderatie & Retentie; het gebruikersportaal bedient Beoordeling (inclusief credits en het waarderen van ontvangen beoordelingen), het melden van vragen en de gebruikerskant van Gebruiker & Toegang.
Zie ook: Backend & API, Media & opname en Termen.
Uitgangspunten
Section titled “Uitgangspunten”- Twee portalen, één API. Twee aparte React-apps op dezelfde deployable API; rolscheiding via autorisatiebeleid en routebeveiliging, niet via gedupliceerde backends.
- Getypeerd van rand tot rand. TypeScript overal; de API-client wordt uit OpenAPI gegenereerd, zodat backend-contracten en frontend-types nooit uit elkaar lopen.
- Eén opinie-flow voor video én foto. De UI behandelt beide
MediaType-waarden gelijk; alleen de capture verschilt (zie Media & opname). - Geen upload. Beeld komt uitsluitend van de live camera via een getypeerde camera-hook.
- Privacy by design. Het gebruikersportaal houdt geen onnodige persoonsgegevens vast; communicatie via mail/sms bestaat niet in de UI (uitsluitend backend-zijdig het optionele e-mailadres voor wachtwoordherstel). Dit borgt de anonimiteit. Er is geen locatie-tracking nodig: de UI vraagt geen Geolocation-/GPS-permissie en heeft geen landveld meer (per
2026-07-11m); een optionele regio komt uit een Keuzelijst. - Mobiel kader. Het gebruikersportaal speelt zich af binnen één centraal mobiel kader; desktop is daarvan een ingelijste variant.
- Server-state los van client-state. Servergegevens lopen via een query-cache (TanStack Query); formulier- en UI-state staan strikt gescheiden.
Monorepo-structuur
Section titled “Monorepo-structuur”De frontend is één monorepo (pnpm workspaces of Turborepo) met twee apps en een set gedeelde libs. De apps bevatten alleen portaal-specifieke schermen en routes; alles wat herbruikbaar is, verhuist naar een lib.
| Pakket | Type | Rol |
|---|---|---|
apps/beheerportaal | App | Beheerschermen: Systeeminstellingen (de instelbare aantallen), SysteemWaarden, Keuzelijst-beheer (incl. secties/attributen), statistieken, moderatie (Meldingen). Vereist de beheerder-policy. |
apps/gebruikersportaal | App | Gebruikersschermen: de opname-/Vraag-flow, opinie geven (scores + een optionele reactie), het resultaat terugzien en de ontvangen beoordelingen waarderen (0–5), credits, anonieme/registratie-toegang. Mobiel kader. |
libs/ui | Lib | Toegankelijke, herbruikbare componenten (knoppen, velden, dialogen, het mobiele-kader-layout, de smiley-/scoreschaal). |
libs/api-client | Lib | De uit OpenAPI gegenereerde getypeerde client + de SignalR-client en TanStack-Query-hooks. |
libs/shared | Lib | Gedeelde TypeScript-types, Zod-schema’s, i18n-resources, de camera-hook en hulpfuncties. |
flowchart TD
subgraph Monorepo["Monorepo (pnpm/Turborepo)"]
subgraph Apps["apps/"]
Beheer["beheerportaal (React 19 + Vite)"]
Gebruiker["gebruikersportaal (React 19 + Vite)"]
end
subgraph Libs["libs/"]
UI["ui (componenten + mobiel kader)"]
ApiClient["api-client (OpenAPI-client + SignalR + Query-hooks)"]
Shared["shared (types, Zod, i18n, camera-hook)"]
end
end
API["I See You-API (.NET, OpenAPI + SignalR)"]
Beheer --> UI
Beheer --> ApiClient
Beheer --> Shared
Gebruiker --> UI
Gebruiker --> ApiClient
Gebruiker --> Shared
ApiClient --> Shared
ApiClient -->|REST + WebSocket| API
Beide portalen delen exact dezelfde
api-client,uienshared. De scheiding zit in de routes en de autorisatie-policy, niet in dubbele code. Eén API genereert één OpenAPI-document; daaruit volgt één getypeerde client voor beide apps.
Routing (React Router)
Section titled “Routing (React Router)”Routing loopt per app via React Router. Elke app heeft een eigen routerboom met een routebeveiliging die controleert of de actieve Sessie aan de vereiste policy voldoet:
- Beheerportaal — routes zijn afgeschermd met een beheerder-guard. Zonder beheerderssessie volgt een redirect naar het inlogscherm.
- Gebruikersportaal — een deel is publiek/anoniem (opname starten, registreren, aanmelden), een deel vereist een gebruikerssessie (Mijn vragen, profiel). Een klik op de eigen naam in de kopbalk opent de profielpagina (profiel, credits, reviewer-statistieken).
De opname-flow in het gebruikersportaal is een reeks stappen die samen één Vraag opbouwen: capture (video van maximaal 10 seconden of foto) → optionele toelichting → Beoordelingsselectie (de maker kiest na de opname welke secties/attributen beoordeeld worden; er is géén open-vragen-blok meer) → optioneel een e-mailadres voor de uitslagnotificatie → maker-profiel → SelectieCriteria → bevestigen — waarbij de vaste inzage-prijs vóór het versturen wordt getoond (er is géén stap “aantal beoordelingen kiezen”: elke vraag krijgt het vaste aantal uit de Systeeminstellingen) — → server-side matching met evenveel opiniegevers als dat vaste aantal. De toelichting wordt bij het indienen door de tekstmoderatie gecontroleerd; een afkeuring toont de UI direct zodat de maker de tekst kan aanpassen. De afzonderlijke schermen zijn portaal-specifieke route-componenten; het scoren zelf gebruikt de score-componenten uit libs/ui. Zie het scenario Matching van opiniegevers.
Er is geen land- of locatieveld meer (per 2026-07-11m — ADR-037): geen GeoIP, geen automatische detectie. Zowel het maker-profiel als de SelectieCriteria-stap tonen wel een optionele regio-dropdown (Keuzelijst-type regio, vlak — geen landprefix meer).
State is bewust gesplitst in server-state en client-state.
| Soort | Aanpak | Toelichting |
|---|---|---|
| Server-state | TanStack Query | Alle data van de API (vragen, opdrachten, opinies, credit-saldo, waarderingen, keuzelijsten, systeeminstellingen) loopt via query/mutation-hooks in libs/api-client: caching, invalidatie, achtergrond-refetch en optimistische updates. De server is de bron van waarheid. De regio-lijst komt eveneens als Keuzelijst van de API. |
| Formulieren | React Hook Form + Zod | Formulier-state (registratie, profiel, beoordelingsselectie, toelichting, het optionele uitslagnotificatie-adres, selectiecriteria, opinie-scores + de optionele reactie, de waardering 0–5, beheerformulieren) via React Hook Form; validatie via Zod-schema’s uit libs/shared, afgestemd op de domeinregels (bv. Score 1..5 én alle gevraagde attributen gescoord (per 2026-07-05; zo gebouwd), waardering 0..5, minstens één gekozen sectie, een geldig e-mailformaat voor het optionele notificatie-adres, leeftijdsrange in SelectieCriteria). De uiteindelijke goedkeuring van vrije tekst blijft server-side (tekstmoderatie); een afkeuring komt als foutrespons terug in het formulier. Bij registratie is een e-mailadres een optioneel veld, uitsluitend voor wachtwoordherstel; zonder e-mail is er geen herstel. |
| Lokale UI-state | React-hooks | Tijdelijke UI-state (stap in de flow, geopende dialoog, capture-status) blijft lokaal in componenten; vluchtig en client-side. |
Het mobiele-kader-profiel uit de opname-flow blijft client-side en vluchtig. Een niet-geregistreerde maker resulteert backend-zijdig in een AnoniemeGebruiker; de bron van waarheid voor registratie en sessies ligt bij de backend, niet bij client-state.
Internationalisatie (i18n)
Section titled “Internationalisatie (i18n)”Teksten lopen via i18next met vier talen als doelbeeld: nl, en, es, fr. De resources staan gedeeld in libs/shared, zodat beide portalen dezelfde vertalingen en taalconventies delen. De actieve taal wordt automatisch bepaald uit navigator.language (de browsertaal) en is corrigeerbaar door de gebruiker; valt de browsertaal buiten de vier ondersteunde talen, dan is nl de standaard. De keuze wordt onthouden in de browser.
Gebouwd (per 2026-07-09): het gebruikersportaal is volledig op i18next + react-i18next aangesloten — álle UI-teksten komen uit een per scherm gegroepeerde catalogus (frontend/src/i18n/nl.ts), met interpolatie, meervoudsvormen en rich-text via <Trans>. Nederlands is de standaard- en vooralsnog enige taal; een extra taal toevoegen = het catalogusbestand vertalen en registreren in frontend/src/i18n/index.ts (TALEN + resources). De gekozen taal wordt bewaard in localStorage (iseeyou.taal); zodra er meer dan één taal geregistreerd is, toont het profielscherm automatisch een taalkiezer. Automatische navigator.language-detectie en het beheerportaal volgen later (het beheerportaal is bewust Nederlandstalig).
| Onderdeel | Bron |
|---|---|
| Statische UI-teksten | i18next-resources in libs/shared (nl/en/es/fr) |
| Regio-lijst | Beheerbare Keuzelijst-stamdata van de API (type regio, per taal) — géén landenlijst meer sinds 2026-07-11m |
| Stamdata-opties (gender, culturele achtergrond) | Keuzelijst van de API, gefilterd op taal + type |
| Actieve taal | Automatisch uit navigator.language (corrigeerbaar; default nl) |
Styling en toegankelijke componenten
Section titled “Styling en toegankelijke componenten”Styling gebeurt met Tailwind CSS, met een gedeelde configuratie in de monorepo zodat beide portalen dezelfde design-tokens (kleuren, spacing, typografie) gebruiken. Interactieve componenten zijn opgebouwd op toegankelijke primitieven (Headless UI of Radix) — dialogen, dropdowns, listboxes, tabs — zodat toetsenbordnavigatie, focusbeheer en ARIA-rollen out of the box correct zijn.
De smiley-/scoreschaal voor het geven van een Opinie is een herbruikbare, toegankelijke radiogroep in libs/ui: schaal 1–5, verplicht per gevraagd attribuut — er is geen “geen mening”-optie en het opinieformulier kan pas verstuurd worden (submit-validatie) als alle gevraagde attributen gescoord zijn (zie Score). (Aanscherping per 2026-07-05; de gebouwde component hanteert deze verplichte 1–5-schaal.) De opiniegever scoort alleen de secties/attributen die de maker via de Beoordelingsselectie heeft gekozen; Gezicht, Kleding en Accessoires zijn daarbij de standaard secties, maar de beschikbare secties/attributen zijn beheerbare stamdata (Keuzelijst). Elk gekozen attribuut scoort op die schaal; de SectieGemiddelden worden afgeleid over alle scores. Naast de scores kan de opiniegever één vrije reactie typen in het veld “Jouw reactie (optioneel)”; dit is optioneel en staat open voor iedereen (geen sterren-drempel meer). De maker gebruikt in Mijn vragen een aparte waarderings-control (0–5 op de smiley-schaal) om de kwaliteit van elke ontvangen beoordeling te scoren — losstaand van de smiley-schaal.
Gegenereerde API-client (OpenAPI)
Section titled “Gegenereerde API-client (OpenAPI)”De API publiceert een OpenAPI-document. Daaruit genereren we een getypeerde client in libs/api-client. Voordelen:
- Eén bron van waarheid voor contracten. Endpoints, request-/response-types en enums (
MediaType,Vraagtype,OpdrachtStatus) komen rechtstreeks uit het backend-contract. Een breaking change in de API geeft direct typefouten in de frontend. - Geen handgeschreven fetch-code. De generatie levert getypeerde functies; daaromheen schrijven we dunne TanStack-Query-hooks zodat schermen alleen met hooks werken, niet met losse HTTP-aanroepen.
- Gedeeld door beide portalen. Eén gegenereerde client bedient beheer- én gebruikersportaal; autorisatie bepaalt welke endpoints een sessie mag aanroepen.
REST wordt gebruikt voor alle state-mutaties; realtime push loopt apart via SignalR (hieronder). De client draagt de cookie-sessie mee; er is geen aparte tokenopslag in de UI.
Realtime: de SignalR-client
Section titled “Realtime: de SignalR-client”Voor realtime updates gebruikt libs/api-client de @microsoft/signalr-client, die verbindt met de SignalR-hubs van de backend. REST blijft de weg voor mutaties; SignalR doet uitsluitend push.
| Hub | Gebruik in de frontend |
|---|---|
| Aanwezigheid | Heartbeat/presence vanuit het gebruikersportaal voedt laatsteBezoek en het online-venster; online gebruikers krijgen voorrang bij de matching (geen filter of keuze). |
| Beoordeling | Notificaties OpdrachtToegewezen / OpdrachtVoltooid / VraagVerlopen: een opiniegever ziet live een nieuwe Opdracht verschijnen en een maker ziet voortgang/verlopen-status zonder te pollen. |
Inkomende events worden vertaald naar query-invalidatie in TanStack Query, zodat de UI-state altijd consistent blijft met de servergegevens. De client is verbindings-veerkrachtig (automatische reconnect) en stelt geen eisen aan welke API-instance hem bedient; bij meerdere instances gebruikt de backend een Redis-backplane (config, geen frontend-herontwerp).
Camera-hook (getUserMedia / MediaRecorder)
Section titled “Camera-hook (getUserMedia / MediaRecorder)”Het vastleggen van beeld gebeurt via een getypeerde camera-hook in libs/shared, gebouwd op getUserMedia en MediaRecorder. De hook abstraheert het verschil tussen de twee MediaType-waarden achter één interface:
- Video — start een opname van maximaal 10 seconden (mag korter) en levert een mediablob op.
- Foto — legt één frame vast en levert een afbeeldingsblob op.
De hook geeft een getypeerde status (idle/aan het opnemen/klaar/fout), bedient permissies en cameraselectie, en levert het resultaat aan de opname-flow. De opinie-flow is identiek voor video en foto; alleen de capture-stap verschilt. Beeld komt uitsluitend van de live camera (geen upload). Het resultaat wordt geüpload als tijdelijke media en daarna gepromoveerd tot Vraag; de exacte stappen staan in Media & opname.
Mobiel-kader-layout
Section titled “Mobiel-kader-layout”Het gebruikersportaal rendert binnen één centraal mobiel kader met telefoonverhouding; het beheerportaal is een reguliere desktop-layout. Het kader is een gedeelde layout-component in libs/ui:
| Weergave | Gedrag |
|---|---|
| Mobiel | Het kader vult het scherm. |
| Desktop | Het kader staat gecentreerd op een achtergrond; bij scrollen blijft de achtergrond op zijn plaats. |
Binnen het kader is er een vaste titelbalk, een scrollbaar inhoudsvlak (beeld/formulier) en een vaste bedieningsbalk onderaan (vervolgstap, opslaan, annuleren).
PWA: installeerbaar op het thuisscherm
Section titled “PWA: installeerbaar op het thuisscherm”(Per 2026-07-11l, routekaart fase 2.) Het gebruikersportaal is een Progressive Web App: een camera-first product hoort op het thuisscherm. Gebouwd met vite-plugin-pwa:
- Web-app-manifest (naam, iconen 192/512 + maskable variant,
display: standalone, merk-themakleur) — het portaal is installeerbaar op mobiel (Android/iOS “zet op beginscherm”) en desktop. - Service worker (Workbox,
generateSW,autoUpdate): cachet alléén de app-shell (de gebouwde statics, ± 0,5 MB incl. lettertypen) met een SPA-navigatiefallback. Alle API- en media-verkeer is cross-origin (VITE_API) en blijft dus altijd netwerk — er worden nooit uitslagen, saldi of opnames gecachet, en de anonimiteits-/retentiebeloften blijven onverkort gelden. Een nieuwe deploy activeert zichzelf (registerType: 'autoUpdate'). - De toegangscode-poort en de camera-opname werken ongewijzigd binnen de geïnstalleerde app (zelfde origin, zelfde permissies).
- Web push is bewust buiten scope — de pushmelding “je uitslag staat klaar” is een aparte vervolgstory (subscripties + VAPID + backend-koppeling aan de uitslagnotificatie).
Het beheerportaal blijft een gewone website (desktop-tool; geen installatie-behoefte).
| Niveau | Gereedschap | Wat |
|---|---|---|
| Unit / componenten | Vitest + Testing Library | Componenten in libs/ui, hooks (incl. de camera-hook met gemockte media-API’s), Zod-schema’s en query-hooks. |
| End-to-end | Playwright | De opname-flow (video én foto, met gemockte camera), opinie geven, registratie/aanmelden, en de beheer-flows in het beheerportaal. |
De gegenereerde API-client maakt het mogelijk om in tests tegen getypeerde mocks te werken die het OpenAPI-contract volgen.
Mapindeling (frontend)
Section titled “Mapindeling (frontend)”frontend/ # monorepo-root (pnpm workspaces / Turborepo)├── apps/│ ├── beheerportaal/ # React 19 + Vite — beheer + moderatie│ └── gebruikersportaal/ # React 19 + Vite — opname, opinie, waardering, credits, toegang├── libs/│ ├── ui/ # toegankelijke componenten + mobiel-kader-layout + scoreschaal│ ├── api-client/ # OpenAPI-gegenereerde client + SignalR-client + Query-hooks│ └── shared/ # types, Zod-schema's, i18next-resources, camera-hook, utils├── package.json # workspace-root└── pnpm-workspace.yaml # (of turbo.json)De koppeling met de server, de endpoints en de SignalR-hubs staan beschreven in Backend & API.
Waarom React (op merites afgewogen)
Section titled “Waarom React (op merites afgewogen)”De keuze voor React 19 is gemaakt op de eisen van I See You — camera-centrische opname, vier talen, twee portalen op één API en een testbare, getypeerde codebase — niet op gewoonte. Een korte afweging tegen alternatieven:
| Optie | Afweging t.o.v. de I See You-eisen |
|---|---|
| React 19 (gekozen) | Grootste ecosysteem voor precies de bouwstenen die we nodig hebben: getypeerde OpenAPI-generatoren, TanStack Query, React Hook Form + Zod, Headless UI/Radix, i18next en een eerste-klas @microsoft/signalr-client. De camera-API’s (getUserMedia/MediaRecorder) zijn web-standaarden; React legt daar niets in de weg en de hook-vorm past natuurlijk bij de capture-statemachine. Breed beschikbare kennis verlaagt het risico op een greenfield-herbouw. |
| Vue | Volwaardig en prettig, maar het ecosysteem rond getypeerde OpenAPI-clients, formuliervalidatie en toegankelijke primitieven is smaller; voor een camera-zware, sterk getypeerde app weegt React’s grotere keuze aan kant-en-klare, goed onderhouden libs zwaarder. |
| Angular | Compleet framework met veel ingebouwd, maar zwaarder en met meer ceremonie dan deze relatief compacte twee-portalen-app vraagt. De flexibiliteit om per onderdeel de beste lib te kiezen (Query, RHF, Radix) is groter in het React-model. |
| Svelte | Zeer efficiënt en beknopt, maar het ecosysteem (vooral getypeerde OpenAPI-tooling, toegankelijke component-primitieven en een officiële realtime-client) is kleiner; bij een app die leunt op generatie en realtime weegt beschikbaarheid en volwassenheid van libs zwaarder dan bundelgrootte. |
| Blazor | Houdt alles in .NET, maar WebAssembly-payloads zijn groter en de toegang tot browser-camera-API’s (getUserMedia/MediaRecorder) verloopt via JS-interop — extra indirectie precies op de meest kritische, camera-centrische functie. Voor een mobiel-kader-UI met live capture is een native JS/TS-stack directer en lichter. |
Kortom: React biedt de meest directe weg naar camera-native capture en de breedste, volwassen verzameling getypeerde, toegankelijke en realtime bouwstenen die deze twee-portalen-monorepo nodig heeft.