feat(WP-67): merge behandelportal into this repo as a monorepo
Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
||||
import * as AsyncStories from '../src/ui/async/async.stories';
|
||||
|
||||
<Meta title="Foundations/FP in the UI" />
|
||||
|
||||
# Functional programming in the UI
|
||||
|
||||
The components in this library are the _view_. Behind them, three small functional tools do
|
||||
the heavy lifting — all so that **illegal states can't be represented**. This page is the
|
||||
Storybook front door; the full narrative lives in `docs/reference/fp-tea-atomic-design.md`, and a
|
||||
side-by-side "before/after" runs at the app's **`/concepts`** route.
|
||||
|
||||
## 1. `RemoteData<E,T>` — async has four states, not a boolean soup
|
||||
|
||||
`src/app/shared/application/remote-data.ts`. Instead of juggling `loading`, `error`, and
|
||||
`data` flags (which permit "loading **and** error" nonsense), one tagged union:
|
||||
`Loading | Empty | Failure | Success`. You combine sources with `map`/`map2`/`andThen` and
|
||||
render it through the `async` molecule — exactly one of four templates shows, by
|
||||
construction:
|
||||
|
||||
<Canvas of={AsyncStories.Loading} />
|
||||
<Canvas of={AsyncStories.ErrorState} />
|
||||
|
||||
## 2. The Elm-style store — all state in one Model, changed only by pure `reduce`
|
||||
|
||||
`src/app/shared/application/store.ts` + the `*.machine.ts` files. State is one tagged-union
|
||||
value; the template never mutates it, it `dispatch`es a message and a **pure**
|
||||
`reduce(model, msg)` returns the next state. Side effects live in a _command_, never in the
|
||||
reducer:
|
||||
|
||||
```ts
|
||||
// reducer = "what the new state is" — pure, testable, no I/O
|
||||
function reduce(model: Model, msg: Msg): Model { … }
|
||||
|
||||
// command = "go do it, then say what happened"
|
||||
async function submit(...) {
|
||||
const res = await http(...);
|
||||
dispatch(res.ok ? { tag: 'Submitted' } : { tag: 'Failed', error: res.error });
|
||||
}
|
||||
```
|
||||
|
||||
Because state is one value, the whole thing is inspectable and every transition has a spec.
|
||||
|
||||
## 3. Parse, don't validate — raw input becomes a branded type once
|
||||
|
||||
`src/app/registratie/domain/value-objects/`. A `Postcode` is a distinct type from `string`,
|
||||
mintable only through `parsePostcode`, which returns a `Result`. Once you hold the type, you
|
||||
never re-check it — the type _is_ the proof. Compose the parse pipeline with the `Result`
|
||||
combinators in `src/app/shared/kernel/fp.ts` (`map`, `mapErr`, `andThen`, `fold`) rather than
|
||||
hand-branching `r.ok ? … : …` at every step.
|
||||
|
||||
```ts
|
||||
parsePostcode(raw) // Result<string, Postcode>
|
||||
|> mapErr(toLocalizedMessage) // swap raw msg → UI copy
|
||||
|> map(toDomain) // only runs on success
|
||||
```
|
||||
|
||||
## How it connects to atomic design
|
||||
|
||||
Atoms and molecules are pure view functions of their inputs; pages are the TEA runtime (the
|
||||
"shell") that holds the store and wires effects. Same inward-pointing discipline as the
|
||||
[layer rule](?path=/docs/foundations-atomic-design--docs), applied to state and effects
|
||||
instead of imports.
|
||||
Reference in New Issue
Block a user