Atomic-design POC: BIG-register self-service portal (Angular + Rijkshuisstijl)

Atoms/molecules/organisms/templates/pages composing the NL Design System
(Utrecht) CSS themed Rijkshuisstijl via @rijkshuisstijl-community tokens.
Login -> dashboard -> registration detail, mock JSON over HttpClient, Storybook
organized by atomic layer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-25 14:23:11 +02:00
commit 033d6f0317
58 changed files with 24306 additions and 0 deletions

18
src/app/app.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { ApplicationConfig, LOCALE_ID, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { registerLocaleData } from '@angular/common';
import localeNl from '@angular/common/locales/nl';
import { routes } from './app.routes';
registerLocaleData(localeNl);
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
{ provide: LOCALE_ID, useValue: 'nl' },
]
};

9
src/app/app.routes.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: 'login' },
{ path: 'login', loadComponent: () => import('./pages/login/login.page').then(m => m.LoginPage) },
{ path: 'dashboard', loadComponent: () => import('./pages/dashboard/dashboard.page').then(m => m.DashboardPage) },
{ path: 'registratie', loadComponent: () => import('./pages/registration-detail/registration-detail.page').then(m => m.RegistrationDetailPage) },
{ path: '**', redirectTo: 'login' },
];

9
src/app/app.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: '<router-outlet />',
})
export class App {}

View File

@@ -0,0 +1,22 @@
import { Component, input } from '@angular/core';
type AlertType = 'info' | 'ok' | 'warning' | 'error';
/** Atom: alert/message banner. */
@Component({
selector: 'app-alert',
template: `
<div
class="utrecht-alert rhc-alert"
[class.utrecht-alert--info]="type() === 'info'"
[class.utrecht-alert--ok]="type() === 'ok'"
[class.utrecht-alert--warning]="type() === 'warning'"
[class.utrecht-alert--error]="type() === 'error'"
role="status">
<ng-content />
</div>
`,
})
export class AlertComponent {
type = input<AlertType>('info');
}

View File

@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { AlertComponent } from './alert.component';
const meta: Meta<AlertComponent> = {
title: 'Atoms/Alert',
component: AlertComponent,
render: (args) => ({
props: args,
template: `<app-alert [type]="type">Uw wijziging is ontvangen.</app-alert>`,
}),
};
export default meta;
type Story = StoryObj<AlertComponent>;
export const Info: Story = { args: { type: 'info' } };
export const Ok: Story = { args: { type: 'ok' } };
export const Warning: Story = { args: { type: 'warning' } };
export const Error: Story = { args: { type: 'error' } };

View File

@@ -0,0 +1,26 @@
import { Component, input } from '@angular/core';
type Variant = 'primary' | 'secondary' | 'subtle' | 'danger';
/** Atom: button. Thin wrapper over the Utrecht/RHC button CSS. */
@Component({
selector: 'app-button',
template: `
<button
[type]="type()"
[disabled]="disabled()"
class="utrecht-button rhc-button"
[class.utrecht-button--primary-action]="variant() === 'primary'"
[class.utrecht-button--secondary-action]="variant() === 'secondary'"
[class.utrecht-button--subtle]="variant() === 'subtle'"
[class.utrecht-button--danger]="variant() === 'danger'"
[class.utrecht-button--disabled]="disabled()">
<ng-content />
</button>
`,
})
export class ButtonComponent {
variant = input<Variant>('primary');
type = input<'button' | 'submit'>('button');
disabled = input(false);
}

View File

@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { ButtonComponent } from './button.component';
const meta: Meta<ButtonComponent> = {
title: 'Atoms/Button',
component: ButtonComponent,
render: (args) => ({
props: args,
template: `<app-button [variant]="variant" [type]="type" [disabled]="disabled">Knop</app-button>`,
}),
};
export default meta;
type Story = StoryObj<ButtonComponent>;
export const Primary: Story = { args: { variant: 'primary' } };
export const Secondary: Story = { args: { variant: 'secondary' } };
export const Subtle: Story = { args: { variant: 'subtle' } };
export const Danger: Story = { args: { variant: 'danger' } };
export const Disabled: Story = { args: { variant: 'primary', disabled: true } };

View File

@@ -0,0 +1,23 @@
import { Component, input } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
/** Atom: heading. Renders the right h1..h5 with RHC heading styling.
Single <ng-content> captured in a template — multiple ng-content across
@switch branches silently drops the projected content. */
@Component({
selector: 'app-heading',
imports: [NgTemplateOutlet],
template: `
<ng-template #content><ng-content /></ng-template>
@switch (level()) {
@case (1) { <h1 class="rhc-heading nl-heading--level-1"><ng-container [ngTemplateOutlet]="content" /></h1> }
@case (2) { <h2 class="rhc-heading nl-heading--level-2"><ng-container [ngTemplateOutlet]="content" /></h2> }
@case (3) { <h3 class="rhc-heading nl-heading--level-3"><ng-container [ngTemplateOutlet]="content" /></h3> }
@case (4) { <h4 class="rhc-heading nl-heading--level-4"><ng-container [ngTemplateOutlet]="content" /></h4> }
@default { <h5 class="rhc-heading nl-heading--level-5"><ng-container [ngTemplateOutlet]="content" /></h5> }
}
`,
})
export class HeadingComponent {
level = input<1 | 2 | 3 | 4 | 5>(2);
}

View File

@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { HeadingComponent } from './heading.component';
const meta: Meta<HeadingComponent> = {
title: 'Atoms/Heading',
component: HeadingComponent,
render: (args) => ({
props: args,
template: `<app-heading [level]="level">Mijn BIG-registratie</app-heading>`,
}),
};
export default meta;
type Story = StoryObj<HeadingComponent>;
export const Level1: Story = { args: { level: 1 } };
export const Level2: Story = { args: { level: 2 } };
export const Level3: Story = { args: { level: 3 } };

View File

@@ -0,0 +1,12 @@
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
/** Atom: link. Internal router link styled as an RHC/Utrecht link. */
@Component({
selector: 'app-link',
imports: [RouterLink],
template: `<a [routerLink]="to()" class="rhc-link nl-link utrecht-link"><ng-content /></a>`,
})
export class LinkComponent {
to = input.required<string>();
}

View File

@@ -0,0 +1,22 @@
import { Component, computed, input } from '@angular/core';
import { RegistrationStatus } from '../../core/models';
/** Atom: status badge = colored RHC dot-badge + label. Hand-built to show how
you compose a new atom from design tokens (dot color driven per status). */
@Component({
selector: 'app-status-badge',
template: `
<span style="display:inline-flex;align-items:center;gap:0.5rem">
<span class="rhc-dot-badge" [style.background-color]="color()" aria-hidden="true"></span>
<span>{{ status() }}</span>
</span>
`,
})
export class StatusBadgeComponent {
status = input.required<RegistrationStatus>();
color = computed(() => ({
Geregistreerd: 'var(--rhc-color-groen-500)',
Doorgehaald: 'var(--rhc-color-rood-500)',
Geschorst: 'var(--rhc-color-oranje-500)',
}[this.status()]));
}

View File

@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { StatusBadgeComponent } from './status-badge.component';
const meta: Meta<StatusBadgeComponent> = {
title: 'Atoms/Status Badge',
component: StatusBadgeComponent,
};
export default meta;
type Story = StoryObj<StatusBadgeComponent>;
export const Geregistreerd: Story = { args: { status: 'Geregistreerd' } };
export const Geschorst: Story = { args: { status: 'Geschorst' } };
export const Doorgehaald: Story = { args: { status: 'Doorgehaald' } };

View File

@@ -0,0 +1,40 @@
import { Component, forwardRef, input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
/** Atom: text input. Utrecht textbox wired up as a form control (ngModel/reactive). */
@Component({
selector: 'app-text-input',
template: `
<input
class="utrecht-textbox rhc-text-input"
[class.utrecht-textbox--invalid]="invalid()"
[type]="type()"
[id]="inputId()"
[placeholder]="placeholder()"
[disabled]="disabled"
[value]="value"
(input)="onInput($event)"
(blur)="onTouched()" />
`,
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true }],
})
export class TextInputComponent implements ControlValueAccessor {
type = input<'text' | 'password' | 'email'>('text');
placeholder = input('');
invalid = input(false);
inputId = input<string>();
value = '';
disabled = false;
onChange: (v: string) => void = () => {};
onTouched: () => void = () => {};
onInput(e: Event) {
this.value = (e.target as HTMLInputElement).value;
this.onChange(this.value);
}
writeValue(v: string) { this.value = v ?? ''; }
registerOnChange(fn: (v: string) => void) { this.onChange = fn; }
registerOnTouched(fn: () => void) { this.onTouched = fn; }
setDisabledState(d: boolean) { this.disabled = d; }
}

17
src/app/core/models.ts Normal file
View File

@@ -0,0 +1,17 @@
export type RegistrationStatus = 'Geregistreerd' | 'Doorgehaald' | 'Geschorst';
export interface Registration {
bigNummer: string;
naam: string;
beroep: string; // arts, verpleegkundige, apotheker, ...
status: RegistrationStatus;
registratiedatum: string; // ISO date
herregistratieDatum: string;
geboortedatum: string;
}
export interface Aantekening {
type: string; // specialisme of aantekening
omschrijving: string;
datum: string;
}

View File

@@ -0,0 +1,17 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Registration, Aantekening } from './models';
@Injectable({ providedIn: 'root' })
export class RegistrationService {
private http = inject(HttpClient);
getRegistration(): Observable<Registration> {
return this.http.get<Registration>('mock/registration.json');
}
getAantekeningen(): Observable<Aantekening[]> {
return this.http.get<Aantekening[]>('mock/notes.json');
}
}

View File

@@ -0,0 +1,17 @@
import { Component, input } from '@angular/core';
/** Molecule: one key/value row inside an RHC data-summary.
Wrap several of these in .rhc-data-summary (see registration-summary). */
@Component({
selector: 'app-data-row',
template: `
<div class="rhc-data-summary__item">
<dt class="rhc-data-summary__item-key">{{ key() }}</dt>
<dd class="rhc-data-summary__item-value"><ng-content>{{ value() }}</ng-content></dd>
</div>
`,
})
export class DataRowComponent {
key = input.required<string>();
value = input<string | null>('');
}

View File

@@ -0,0 +1,25 @@
import { Component, input } from '@angular/core';
/** Molecule: form field = label + projected control + optional error/description.
Reused by both the login form and the change-request form. */
@Component({
selector: 'app-form-field',
template: `
<div class="rhc-form-field utrecht-form-field" [class.utrecht-form-field--invalid]="!!error()">
<label class="utrecht-form-label" [for]="fieldId()">{{ label() }}</label>
@if (description()) {
<div class="utrecht-form-field-description">{{ description() }}</div>
}
<ng-content />
@if (error()) {
<div class="utrecht-form-field-error-message" role="alert">{{ error() }}</div>
}
</div>
`,
})
export class FormFieldComponent {
label = input.required<string>();
fieldId = input.required<string>();
description = input<string>();
error = input<string>();
}

View File

@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { FormFieldComponent } from './form-field.component';
import { TextInputComponent } from '../../atoms/text-input/text-input.component';
const meta: Meta<FormFieldComponent> = {
title: 'Molecules/Form Field',
component: FormFieldComponent,
decorators: [moduleMetadata({ imports: [TextInputComponent] })],
render: (args) => ({
props: args,
template: `
<app-form-field [label]="label" [fieldId]="fieldId" [description]="description" [error]="error">
<app-text-input [inputId]="fieldId" [invalid]="!!error" placeholder="Vul in" />
</app-form-field>`,
}),
};
export default meta;
type Story = StoryObj<FormFieldComponent>;
export const Default: Story = {
args: { label: 'BSN', fieldId: 'bsn', description: '9 cijfers' },
};
export const WithError: Story = {
args: { label: 'Straat en huisnummer', fieldId: 'street', error: 'Dit veld is verplicht.' },
};

View File

@@ -0,0 +1,43 @@
import { Component, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '../../molecules/form-field/form-field.component';
import { TextInputComponent } from '../../atoms/text-input/text-input.component';
import { ButtonComponent } from '../../atoms/button/button.component';
import { HeadingComponent } from '../../atoms/heading/heading.component';
/** Organism: change-request (adreswijziging) form. Reuses the same form-field
molecule + text-input/button atoms as the login form. */
@Component({
selector: 'app-change-request-form',
imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent],
template: `
<app-heading [level]="2">Adreswijziging doorgeven</app-heading>
<form (ngSubmit)="onSubmit()" style="max-width:28rem">
<app-form-field label="Straat en huisnummer" fieldId="street" [error]="streetError()">
<app-text-input inputId="street" [(ngModel)]="street" name="street" [invalid]="!!streetError()" />
</app-form-field>
<app-form-field label="Postcode" fieldId="zip">
<app-text-input inputId="zip" [(ngModel)]="zip" name="zip" placeholder="1234 AB" />
</app-form-field>
<app-form-field label="Woonplaats" fieldId="city">
<app-text-input inputId="city" [(ngModel)]="city" name="city" />
</app-form-field>
<div style="margin-top:1rem">
<app-button type="submit" variant="primary">Wijziging indienen</app-button>
</div>
</form>
`,
})
export class ChangeRequestFormComponent {
street = '';
zip = '';
city = '';
streetError = signal('');
submitted = output<void>();
onSubmit() {
if (!this.street.trim()) { this.streetError.set('Vul straat en huisnummer in.'); return; }
this.streetError.set('');
this.submitted.emit();
}
}

View File

@@ -0,0 +1,35 @@
import { Component, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormFieldComponent } from '../../molecules/form-field/form-field.component';
import { TextInputComponent } from '../../atoms/text-input/text-input.component';
import { ButtonComponent } from '../../atoms/button/button.component';
import { HeadingComponent } from '../../atoms/heading/heading.component';
/** Organism: DigiD-style mock login. No real auth — just composes atoms/molecules. */
@Component({
selector: 'app-login-form',
imports: [FormsModule, FormFieldComponent, TextInputComponent, ButtonComponent, HeadingComponent],
template: `
<form (ngSubmit)="submit.emit()" style="max-width:24rem">
<app-heading [level]="1">Inloggen</app-heading>
<p class="rhc-paragraph">Log in op uw persoonlijke BIG-register omgeving.</p>
<app-form-field label="BSN" fieldId="bsn" description="9 cijfers (demo: vul iets in)">
<app-text-input inputId="bsn" [(ngModel)]="bsn" name="bsn" placeholder="123456789" />
</app-form-field>
<app-form-field label="Wachtwoord" fieldId="pw">
<app-text-input inputId="pw" type="password" [(ngModel)]="password" name="pw" />
</app-form-field>
<div style="margin-top:1rem">
<app-button type="submit" variant="primary">Inloggen met DigiD</app-button>
</div>
</form>
`,
})
export class LoginFormComponent {
bsn = '';
password = '';
submit = output<void>();
}

View File

@@ -0,0 +1,11 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { LoginFormComponent } from './login-form.component';
const meta: Meta<LoginFormComponent> = {
title: 'Organisms/Login Form',
component: LoginFormComponent,
};
export default meta;
type Story = StoryObj<LoginFormComponent>;
export const Default: Story = {};

View File

@@ -0,0 +1,28 @@
import { Component, input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Registration } from '../../core/models';
import { DataRowComponent } from '../../molecules/data-row/data-row.component';
import { StatusBadgeComponent } from '../../atoms/status-badge/status-badge.component';
/** Organism: registration summary card. Composes data-row molecules + status-badge atom. */
@Component({
selector: 'app-registration-summary',
imports: [DatePipe, DataRowComponent, StatusBadgeComponent],
template: `
<div class="rhc-card rhc-card--default">
<dl class="rhc-data-summary rhc-data-summary--row">
<app-data-row key="BIG-nummer" [value]="reg().bigNummer" />
<app-data-row key="Naam" [value]="reg().naam" />
<app-data-row key="Beroep" [value]="reg().beroep" />
<app-data-row key="Status">
<app-status-badge [status]="reg().status" />
</app-data-row>
<app-data-row key="Registratiedatum" [value]="reg().registratiedatum | date:'longDate'" />
<app-data-row key="Uiterste herregistratie" [value]="reg().herregistratieDatum | date:'longDate'" />
</dl>
</div>
`,
})
export class RegistrationSummaryComponent {
reg = input.required<Registration>();
}

View File

@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { RegistrationSummaryComponent } from './registration-summary.component';
import { Registration } from '../../core/models';
const sample: Registration = {
bigNummer: '19012345601',
naam: 'Dr. A. (Anna) de Vries',
beroep: 'Arts',
status: 'Geregistreerd',
registratiedatum: '2012-09-01',
herregistratieDatum: '2027-09-01',
geboortedatum: '1985-03-14',
};
const meta: Meta<RegistrationSummaryComponent> = {
title: 'Organisms/Registration Summary',
component: RegistrationSummaryComponent,
};
export default meta;
type Story = StoryObj<RegistrationSummaryComponent>;
export const Default: Story = { args: { reg: sample } };
export const Geschorst: Story = { args: { reg: { ...sample, status: 'Geschorst' } } };

View File

@@ -0,0 +1,34 @@
import { Component, input } from '@angular/core';
import { DatePipe } from '@angular/common';
import { Aantekening } from '../../core/models';
/** Organism: table of specialismen/aantekeningen. */
@Component({
selector: 'app-registration-table',
imports: [DatePipe],
template: `
<div class="utrecht-table-container utrecht-table-container--overflow-inline">
<table class="utrecht-table utrecht-table--html-table utrecht-table--alternate-row-color">
<thead>
<tr>
<th class="utrecht-table__header-cell">Type</th>
<th class="utrecht-table__header-cell">Omschrijving</th>
<th class="utrecht-table__header-cell">Datum</th>
</tr>
</thead>
<tbody>
@for (row of rows(); track row.omschrijving) {
<tr>
<td class="utrecht-table__cell">{{ row.type }}</td>
<td class="utrecht-table__cell">{{ row.omschrijving }}</td>
<td class="utrecht-table__cell">{{ row.datum | date:'mediumDate' }}</td>
</tr>
}
</tbody>
</table>
</div>
`,
})
export class RegistrationTableComponent {
rows = input.required<Aantekening[]>();
}

View File

@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { RegistrationTableComponent } from './registration-table.component';
const meta: Meta<RegistrationTableComponent> = {
title: 'Organisms/Registration Table',
component: RegistrationTableComponent,
};
export default meta;
type Story = StoryObj<RegistrationTableComponent>;
export const Default: Story = {
args: {
rows: [
{ type: 'Specialisme', omschrijving: 'Huisartsgeneeskunde', datum: '2016-04-12' },
{ type: 'Aantekening', omschrijving: 'Erkend opleider huisartsgeneeskunde', datum: '2019-01-08' },
],
},
};

View File

@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
/** Organism: site footer. */
@Component({
selector: 'app-site-footer',
template: `
<footer class="utrecht-page-footer" style="background:var(--rhc-color-lintblauw-900,#01689b);color:#fff;margin-top:3rem">
<div style="max-width:64rem;margin:0 auto;padding:1.5rem;display:flex;gap:2rem;flex-wrap:wrap">
<span>BIG-register</span>
<span>CIBG — Ministerie van Volksgezondheid, Welzijn en Sport</span>
<span style="margin-left:auto;opacity:0.85">Demo / POC — geen echte gegevens</span>
</div>
</footer>
`,
})
export class SiteFooterComponent {}

View File

@@ -0,0 +1,25 @@
import { Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
/** Organism: site header with Rijksoverheid-style wordmark + title.
ponytail: text wordmark instead of the licensed Rijksoverheid logo. */
@Component({
selector: 'app-site-header',
imports: [RouterLink],
template: `
<header class="utrecht-page-header" style="background:var(--rhc-color-lintblauw-700,#154273);color:#fff">
<div style="display:flex;align-items:center;gap:1rem;max-width:64rem;margin:0 auto;padding:1rem 1.5rem">
<a routerLink="/dashboard" style="display:flex;align-items:center;gap:0.75rem;color:inherit;text-decoration:none">
<span aria-hidden="true" style="display:inline-block;width:0.5rem;height:2.25rem;background:#fff"></span>
<span style="font-weight:700;line-height:1.1">
Rijksoverheid<br><span style="font-weight:400;font-size:0.9rem">BIG-register</span>
</span>
</a>
<span style="margin-left:auto;font-weight:500">{{ subtitle() }}</span>
</div>
</header>
`,
})
export class SiteHeaderComponent {
subtitle = input('Mijn omgeving');
}

View File

@@ -0,0 +1,38 @@
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { PageLayoutComponent } from '../../templates/page-layout/page-layout.component';
import { HeadingComponent } from '../../atoms/heading/heading.component';
import { LinkComponent } from '../../atoms/link/link.component';
import { RegistrationSummaryComponent } from '../../organisms/registration-summary/registration-summary.component';
import { RegistrationTableComponent } from '../../organisms/registration-table/registration-table.component';
import { RegistrationService } from '../../core/registration.service';
@Component({
selector: 'app-dashboard-page',
imports: [AsyncPipe, PageLayoutComponent, HeadingComponent, LinkComponent, RegistrationSummaryComponent, RegistrationTableComponent],
template: `
<app-page-layout>
<app-heading [level]="1">Mijn BIG-registratie</app-heading>
@if (reg$ | async; as reg) {
<app-registration-summary [reg]="reg" />
}
<div style="margin-top:2rem">
<app-heading [level]="2">Specialismen en aantekeningen</app-heading>
@if (notes$ | async; as notes) {
<app-registration-table [rows]="notes" />
}
</div>
<p style="margin-top:2rem">
<app-link to="/registratie">Gegevens bekijken of een wijziging doorgeven →</app-link>
</p>
</app-page-layout>
`,
})
export class DashboardPage {
private svc = inject(RegistrationService);
reg$ = this.svc.getRegistration();
notes$ = this.svc.getAantekeningen();
}

View File

@@ -0,0 +1,18 @@
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { PageLayoutComponent } from '../../templates/page-layout/page-layout.component';
import { LoginFormComponent } from '../../organisms/login-form/login-form.component';
@Component({
selector: 'app-login-page',
imports: [PageLayoutComponent, LoginFormComponent],
template: `
<app-page-layout>
<app-login-form (submit)="login()" />
</app-page-layout>
`,
})
export class LoginPage {
private router = inject(Router);
login() { this.router.navigate(['/dashboard']); }
}

View File

@@ -0,0 +1,37 @@
import { Component, inject, signal } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { PageLayoutComponent } from '../../templates/page-layout/page-layout.component';
import { HeadingComponent } from '../../atoms/heading/heading.component';
import { LinkComponent } from '../../atoms/link/link.component';
import { AlertComponent } from '../../atoms/alert/alert.component';
import { RegistrationSummaryComponent } from '../../organisms/registration-summary/registration-summary.component';
import { ChangeRequestFormComponent } from '../../organisms/change-request-form/change-request-form.component';
import { RegistrationService } from '../../core/registration.service';
@Component({
selector: 'app-registration-detail-page',
imports: [AsyncPipe, PageLayoutComponent, HeadingComponent, LinkComponent, AlertComponent, RegistrationSummaryComponent, ChangeRequestFormComponent],
template: `
<app-page-layout>
<p><app-link to="/dashboard">← Terug naar overzicht</app-link></p>
<app-heading [level]="1">Mijn gegevens</app-heading>
@if (reg$ | async; as reg) {
<app-registration-summary [reg]="reg" />
}
<div style="margin-top:2rem">
@if (submitted()) {
<app-alert type="ok">Uw adreswijziging is ontvangen. U ontvangt binnen 5 werkdagen bericht.</app-alert>
} @else {
<app-change-request-form (submitted)="submitted.set(true)" />
}
</div>
</app-page-layout>
`,
})
export class RegistrationDetailPage {
private svc = inject(RegistrationService);
reg$ = this.svc.getRegistration();
submitted = signal(false);
}

View File

@@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { SiteHeaderComponent } from '../../organisms/site-header/site-header.component';
import { SiteFooterComponent } from '../../organisms/site-footer/site-footer.component';
/** Template: header + centered content + footer. Wraps every page. */
@Component({
selector: 'app-page-layout',
imports: [SiteHeaderComponent, SiteFooterComponent],
template: `
<a href="#main" class="rhc-skip-link" style="position:absolute;left:-999px">Naar de inhoud</a>
<div class="utrecht-page-layout" style="display:flex;flex-direction:column;min-height:100vh">
<app-site-header />
<main id="main" class="utrecht-page-content" style="flex:1;max-width:64rem;width:100%;margin:0 auto;padding:2rem 1.5rem;box-sizing:border-box">
<ng-content />
</main>
<app-site-footer />
</div>
`,
})
export class PageLayoutComponent {}

19
src/index.html Normal file
View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<title>BIG-register — Mijn omgeving</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Free substitute for RijksSansVF (the licensed Rijksoverheid font). The
design tokens already fall back to 'Fira Sans'. ponytail: no @font-face dance. -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<!-- rhc-theme = component tokens, lintblauw = Rijksoverheid lint-blue palette -->
<body class="rhc-theme lintblauw utrecht-document">
<app-root></app-root>
</body>
</html>

6
src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

8
src/styles.scss Normal file
View File

@@ -0,0 +1,8 @@
/* Rijkshuisstijl design tokens (already themed) + Utrecht/RHC component CSS.
ponytail: we import the published CSS instead of hand-writing styles — the
whole point is that the design system gives us Rijkshuisstijl for free. */
@import '@rijkshuisstijl-community/design-tokens/dist/index.css'; /* .rhc-theme tokens */
@import '@rijkshuisstijl-community/design-tokens/dist/lintblauw/index.css'; /* .lintblauw palette */
@import '@rijkshuisstijl-community/components-css/dist/index.css'; /* component classes */
html, body { margin: 0; min-height: 100%; }