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:
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
ij_typescript_use_double_quotes = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
/node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/mcp.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
*storybook.log
|
||||||
|
storybook-static
|
||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# @storybook/angular's peer range lags Angular 22; the builder works fine at runtime.
|
||||||
|
# ponytail: one flag beats pinning the whole app back to Angular 21.
|
||||||
|
legacy-peer-deps=true
|
||||||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.html",
|
||||||
|
"options": {
|
||||||
|
"parser": "angular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
.storybook/main.ts
Normal file
15
.storybook/main.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/angular';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
"stories": [
|
||||||
|
"../src/**/*.mdx",
|
||||||
|
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-a11y",
|
||||||
|
"@storybook/addon-docs",
|
||||||
|
"@storybook/addon-onboarding"
|
||||||
|
],
|
||||||
|
"framework": "@storybook/angular"
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
3
.storybook/preview-head.html
Normal file
3
.storybook/preview-head.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<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" />
|
||||||
26
.storybook/preview.ts
Normal file
26
.storybook/preview.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Preview } from '@storybook/angular';
|
||||||
|
import { componentWrapperDecorator } from '@storybook/angular';
|
||||||
|
import { setCompodocJson } from '@storybook/addon-docs/angular';
|
||||||
|
import docJson from '../documentation.json';
|
||||||
|
setCompodocJson(docJson);
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
// Apply the Rijkshuisstijl theme classes around every story so the design
|
||||||
|
// tokens cascade (global component CSS comes from the app's build target).
|
||||||
|
decorators: [
|
||||||
|
componentWrapperDecorator(
|
||||||
|
(story) => `<div class="rhc-theme lintblauw utrecht-document" style="padding:1.5rem">${story}</div>`
|
||||||
|
),
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
10
.storybook/tsconfig.doc.json
Normal file
10
.storybook/tsconfig.doc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// This tsconfig is used by Compodoc to generate the documentation for the project.
|
||||||
|
// If Compodoc is not used, this file can be deleted.
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
// Exclude all files that are not needed for documentation generation.
|
||||||
|
"exclude": ["../src/test.ts", "../src/**/*.spec.ts", "../src/**/*.stories.ts"],
|
||||||
|
// Please make sure to include all files from which Compodoc should generate documentation.
|
||||||
|
"include": ["../src/**/*"],
|
||||||
|
"files": ["./typings.d.ts"]
|
||||||
|
}
|
||||||
12
.storybook/tsconfig.json
Normal file
12
.storybook/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"],
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"exclude": ["../src/test.ts", "../src/**/*.spec.ts"],
|
||||||
|
"include": ["../src/**/*.stories.*", "./preview.ts"],
|
||||||
|
"files": ["./typings.d.ts"]
|
||||||
|
}
|
||||||
4
.storybook/typings.d.ts
vendored
Normal file
4
.storybook/typings.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.md' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||||
|
"recommendations": ["angular.ng-template"]
|
||||||
|
}
|
||||||
20
.vscode/launch.json
vendored
Normal file
20
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "ng serve",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: start",
|
||||||
|
"url": "http://localhost:4200/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ng test",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "npm: test",
|
||||||
|
"url": "http://localhost:9876/debug.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
42
.vscode/tasks.json
vendored
Normal file
42
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "start",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "Changes detected"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation (complete|failed)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "test",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": {
|
||||||
|
"owner": "typescript",
|
||||||
|
"pattern": "$tsc",
|
||||||
|
"background": {
|
||||||
|
"activeOnStart": true,
|
||||||
|
"beginsPattern": {
|
||||||
|
"regexp": "Changes detected"
|
||||||
|
},
|
||||||
|
"endsPattern": {
|
||||||
|
"regexp": "bundle generation (complete|failed)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# BIG-register Self Service Portal — Atomic Design POC
|
||||||
|
|
||||||
|
An Angular POC showing how **atomic design** makes a frontend cheap to build and
|
||||||
|
reuse. Domain: the **BIG-register** self-service portal (NL register of healthcare
|
||||||
|
professionals). Styled with the **Rijkshuisstijl** via the NL Design System /
|
||||||
|
Utrecht component CSS — no hand-written theme.
|
||||||
|
|
||||||
|
## The atomic-design story (folder = layer)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/app/
|
||||||
|
atoms/ button · text-input · heading · link · alert · status-badge
|
||||||
|
molecules/ form-field (label+input+error) · data-row
|
||||||
|
organisms/ site-header · site-footer · login-form · registration-summary
|
||||||
|
registration-table · change-request-form
|
||||||
|
templates/ page-layout (header + content + footer)
|
||||||
|
pages/ login · dashboard · registration-detail
|
||||||
|
core/ models · registration.service (HttpClient → public/mock/*.json)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reuse in action** — the same building blocks appear everywhere:
|
||||||
|
- `page-layout` wraps all three pages.
|
||||||
|
- `site-header` + `site-footer` render on every page.
|
||||||
|
- `form-field` + `text-input` + `button` are shared by the login form *and* the
|
||||||
|
change-request form.
|
||||||
|
- `status-badge` shows on both the dashboard and the detail page.
|
||||||
|
|
||||||
|
Adding a new page is just composing existing organisms inside the layout — see
|
||||||
|
`pages/` for how little code each page is.
|
||||||
|
|
||||||
|
## Styling / theming
|
||||||
|
|
||||||
|
`@rijkshuisstijl-community/design-tokens` + `@rijkshuisstijl-community/components-css`
|
||||||
|
provide Rijkshuisstijl-themed Utrecht components as CSS. The theme is applied by the
|
||||||
|
`rhc-theme lintblauw` classes on `<body>` (see `src/index.html`). Atoms are thin
|
||||||
|
Angular wrappers that apply these CSS classes — so the design system does the visual
|
||||||
|
work and we only own the component API.
|
||||||
|
|
||||||
|
> POC shortcuts: free **Fira Sans** stands in for the licensed Rijksoverheid font, a
|
||||||
|
> text wordmark stands in for the logo, login is mocked, and data is static JSON.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm start # ng serve → http://localhost:4200 (login → dashboard → detail)
|
||||||
|
npm run storybook # component library, organized by atomic layer
|
||||||
|
```
|
||||||
108
angular.json
Normal file
108
angular.json
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"cli": {
|
||||||
|
"packageManager": "npm"
|
||||||
|
},
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"atomic-design-poc": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular/build:application",
|
||||||
|
"options": {
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "public"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "1.5MB",
|
||||||
|
"maximumError": "2MB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "4kB",
|
||||||
|
"maximumError": "8kB"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular/build:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "atomic-design-poc:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "atomic-design-poc:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular/build:unit-test"
|
||||||
|
},
|
||||||
|
"storybook": {
|
||||||
|
"builder": "@storybook/angular:start-storybook",
|
||||||
|
"options": {
|
||||||
|
"configDir": ".storybook",
|
||||||
|
"browserTarget": "atomic-design-poc:build",
|
||||||
|
"compodoc": true,
|
||||||
|
"compodocArgs": [
|
||||||
|
"-e",
|
||||||
|
"json",
|
||||||
|
"-d",
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"port": 6006
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build-storybook": {
|
||||||
|
"builder": "@storybook/angular:build-storybook",
|
||||||
|
"options": {
|
||||||
|
"configDir": ".storybook",
|
||||||
|
"browserTarget": "atomic-design-poc:build",
|
||||||
|
"compodoc": true,
|
||||||
|
"compodocArgs": [
|
||||||
|
"-e",
|
||||||
|
"json",
|
||||||
|
"-d",
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"outputDir": "storybook-static"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2110
documentation.json
Normal file
2110
documentation.json
Normal file
File diff suppressed because it is too large
Load Diff
20974
package-lock.json
generated
Normal file
20974
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "atomic-design-poc",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test",
|
||||||
|
"storybook": "ng run atomic-design-poc:storybook",
|
||||||
|
"build-storybook": "ng run atomic-design-poc:build-storybook"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "npm@11.12.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/common": "^22.0.0",
|
||||||
|
"@angular/compiler": "^22.0.0",
|
||||||
|
"@angular/core": "^22.0.0",
|
||||||
|
"@angular/forms": "^22.0.0",
|
||||||
|
"@angular/platform-browser": "^22.0.0",
|
||||||
|
"@angular/router": "^22.0.0",
|
||||||
|
"@rijkshuisstijl-community/components-css": "^17.1.2",
|
||||||
|
"@rijkshuisstijl-community/design-tokens": "^16.1.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/build": "^22.0.4",
|
||||||
|
"@angular/cli": "^22.0.4",
|
||||||
|
"@angular/compiler-cli": "^22.0.0",
|
||||||
|
"jsdom": "^28.0.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vitest": "^4.0.8",
|
||||||
|
"storybook": "^10.4.6",
|
||||||
|
"@storybook/angular": "^10.4.6",
|
||||||
|
"@storybook/addon-a11y": "^10.4.6",
|
||||||
|
"@storybook/addon-docs": "^10.4.6",
|
||||||
|
"@storybook/addon-onboarding": "^10.4.6",
|
||||||
|
"@angular-devkit/build-angular": "^22.0.0",
|
||||||
|
"@angular-devkit/architect": "^0.2200.0",
|
||||||
|
"@angular-devkit/core": "^22.0.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^22.0.0",
|
||||||
|
"@compodoc/compodoc": "^1.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
5
public/mock/notes.json
Normal file
5
public/mock/notes.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{ "type": "Specialisme", "omschrijving": "Huisartsgeneeskunde", "datum": "2016-04-12" },
|
||||||
|
{ "type": "Aantekening", "omschrijving": "Erkend opleider huisartsgeneeskunde", "datum": "2019-01-08" },
|
||||||
|
{ "type": "Specialisme", "omschrijving": "Spoedeisende hulp (kaderopleiding)", "datum": "2021-06-30" }
|
||||||
|
]
|
||||||
9
public/mock/registration.json
Normal file
9
public/mock/registration.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"bigNummer": "19012345601",
|
||||||
|
"naam": "Dr. A. (Anna) de Vries",
|
||||||
|
"beroep": "Arts",
|
||||||
|
"status": "Geregistreerd",
|
||||||
|
"registratiedatum": "2012-09-01",
|
||||||
|
"herregistratieDatum": "2027-09-01",
|
||||||
|
"geboortedatum": "1985-03-14"
|
||||||
|
}
|
||||||
18
src/app/app.config.ts
Normal file
18
src/app/app.config.ts
Normal 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
9
src/app/app.routes.ts
Normal 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
9
src/app/app.ts
Normal 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 {}
|
||||||
22
src/app/atoms/alert/alert.component.ts
Normal file
22
src/app/atoms/alert/alert.component.ts
Normal 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');
|
||||||
|
}
|
||||||
18
src/app/atoms/alert/alert.stories.ts
Normal file
18
src/app/atoms/alert/alert.stories.ts
Normal 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' } };
|
||||||
26
src/app/atoms/button/button.component.ts
Normal file
26
src/app/atoms/button/button.component.ts
Normal 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);
|
||||||
|
}
|
||||||
19
src/app/atoms/button/button.stories.ts
Normal file
19
src/app/atoms/button/button.stories.ts
Normal 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 } };
|
||||||
23
src/app/atoms/heading/heading.component.ts
Normal file
23
src/app/atoms/heading/heading.component.ts
Normal 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);
|
||||||
|
}
|
||||||
17
src/app/atoms/heading/heading.stories.ts
Normal file
17
src/app/atoms/heading/heading.stories.ts
Normal 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 } };
|
||||||
12
src/app/atoms/link/link.component.ts
Normal file
12
src/app/atoms/link/link.component.ts
Normal 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>();
|
||||||
|
}
|
||||||
22
src/app/atoms/status-badge/status-badge.component.ts
Normal file
22
src/app/atoms/status-badge/status-badge.component.ts
Normal 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()]));
|
||||||
|
}
|
||||||
13
src/app/atoms/status-badge/status-badge.stories.ts
Normal file
13
src/app/atoms/status-badge/status-badge.stories.ts
Normal 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' } };
|
||||||
40
src/app/atoms/text-input/text-input.component.ts
Normal file
40
src/app/atoms/text-input/text-input.component.ts
Normal 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
17
src/app/core/models.ts
Normal 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;
|
||||||
|
}
|
||||||
17
src/app/core/registration.service.ts
Normal file
17
src/app/core/registration.service.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/app/molecules/data-row/data-row.component.ts
Normal file
17
src/app/molecules/data-row/data-row.component.ts
Normal 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>('');
|
||||||
|
}
|
||||||
25
src/app/molecules/form-field/form-field.component.ts
Normal file
25
src/app/molecules/form-field/form-field.component.ts
Normal 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>();
|
||||||
|
}
|
||||||
26
src/app/molecules/form-field/form-field.stories.ts
Normal file
26
src/app/molecules/form-field/form-field.stories.ts
Normal 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.' },
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/app/organisms/login-form/login-form.component.ts
Normal file
35
src/app/organisms/login-form/login-form.component.ts
Normal 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>();
|
||||||
|
}
|
||||||
11
src/app/organisms/login-form/login-form.stories.ts
Normal file
11
src/app/organisms/login-form/login-form.stories.ts
Normal 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 = {};
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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' } } };
|
||||||
@@ -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[]>();
|
||||||
|
}
|
||||||
@@ -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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
16
src/app/organisms/site-footer/site-footer.component.ts
Normal file
16
src/app/organisms/site-footer/site-footer.component.ts
Normal 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 {}
|
||||||
25
src/app/organisms/site-header/site-header.component.ts
Normal file
25
src/app/organisms/site-header/site-header.component.ts
Normal 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');
|
||||||
|
}
|
||||||
38
src/app/pages/dashboard/dashboard.page.ts
Normal file
38
src/app/pages/dashboard/dashboard.page.ts
Normal 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();
|
||||||
|
}
|
||||||
18
src/app/pages/login/login.page.ts
Normal file
18
src/app/pages/login/login.page.ts
Normal 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']); }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
20
src/app/templates/page-layout/page-layout.component.ts
Normal file
20
src/app/templates/page-layout/page-layout.component.ts
Normal 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
19
src/index.html
Normal 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
6
src/main.ts
Normal 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
8
src/styles.scss
Normal 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%; }
|
||||||
16
tsconfig.app.json
Normal file
16
tsconfig.app.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.stories.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
31
tsconfig.json
Normal file
31
tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "preserve"
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
tsconfig.spec.json
Normal file
15
tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||||
|
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"vitest/globals"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user