Skip to main content

PLAN-001: Supply display v1 — Red Cross

IMPLEMENTATION RULES: Before implementing this plan, read and follow:

Status: Completed​

Goal: Build the first supply-display surfaces in the Atlas frontend — engineering-validation page for the Red Cross ingest, an /ngo index, full per-NGO landing pages for Red Cross (/ngo/redcross/...), a stub template for the 10 not-yet-ingested NGOs, and a "Lokale tilbud" extension to /kommuner/[kommune_nr]. Adopt Tailwind v4 + shadcn/ui + Designsystemet tokens as the new visual stack alongside this work.

Last Updated: 2026-04-24

Completed: 2026-04-24 — user confirmed "i can see the data now :)" after smoke-testing the live site at http://localhost:4000.

Investigation: INVESTIGATE-supply-frontend-display.md — Sections C, D, E, F.3, H, J + Appendices A and B.

Estimated effort: 10–14 hours focused. Actual: ~6h end-to-end including the Next 16 upgrade folded in mid-PLAN.


Dependencies​

  • Prerequisite (must already be merged): PLAN-002 from INVESTIGATE-ngo-supply-data-model.md. ✅ Already shipped — dim_chapter, dim_activity, fact_chapter_activities populated for Red Cross.
  • Not blocked by: the scraping infrastructure PLAN, the multi-NGO model extensions PLAN, or the Folkehjelp ingest PLANs. Each adds capabilities that this PLAN can consume later but doesn't require.
  • raw.ingest_runs not yet present: Section 6 of the admin validation page (Phase 3) shows "Awaiting infrastructure PLAN" until the scraping infra PLAN ships. Not blocking.
  • dim_chapter.source_url not yet present: chapter detail page links out via dim_ngo.website_url only (no per-chapter deep-link). Lands when the multi-NGO extensions PLAN ships.

Overview​

This is the first supply-display PLAN. It validates that the Red Cross data we imported via PLAN-002 looks right (admin page) and gives Inger and Arne a useful view of "Red Cross from outside" (per-NGO landing pages). It also extends the existing /kommuner/[kommune_nr] route with the first cross-NGO supply section (single NGO for now; the section grows as more NGOs ingest).

This PLAN is also the moment we adopt Tailwind v4 + shadcn/ui + Designsystemet tokens as Atlas's frontend stack. The existing 5 demand-side routes are not touched in v1 — they migrate later.


Phase 1: Stack setup — Tailwind v4 + shadcn/ui + Designsystemet tokens — DONE​

Tasks​

  • 1.1 Install dependencies. From repo root:
    • Add: tailwindcss@^4, @tailwindcss/postcss, class-variance-authority, clsx, tailwind-merge, lucide-react.
    • Already present: @digdir/designsystemet-css@^1.13.2 — keep.
    • Remove: @digdir/designsystemet-react from package.json dependencies (currently unused).
  • 1.2 Configure PostCSS. Create postcss.config.mjs at repo root with @tailwindcss/postcss as the only plugin.
  • 1.3 Generate the Designsystemet theme:
    npx @digdir/designsystemet tokens create \
    --main-colors "accent:#0062BA" \
    --neutral-color "#1E2B3C" \
    --border-radius 4 \
    --theme atlas
    npx @digdir/designsystemet tokens build
    Output goes to design-tokens/atlas/ (created by the CLI). Add to .gitignore? No — commit the generated CSS so the build is deterministic. See investigation [Q26] for the brand colour decision.
  • 1.4 Wire app/globals.css:
    @import "tailwindcss";
    @import "@digdir/designsystemet-css/theme/designsystemet.css";
    @import "@digdir/designsystemet-css/theme/designsystemet.tailwind.css";

    @theme {
    /* shadcn semantic names → Designsystemet role-based tokens */
    --color-background: var(--ds-color-background-default);
    --color-foreground: var(--ds-color-neutral-text-default);
    --color-primary: var(--ds-color-accent-base-default);
    --color-primary-foreground: var(--ds-color-accent-base-contrast-default);
    --color-secondary: var(--ds-color-neutral-surface-default);
    --color-secondary-foreground: var(--ds-color-neutral-text-default);
    --color-muted: var(--ds-color-neutral-surface-tinted);
    --color-muted-foreground: var(--ds-color-neutral-text-subtle);
    --color-accent: var(--ds-color-accent-surface-default);
    --color-accent-foreground: var(--ds-color-accent-text-default);
    --color-destructive: var(--ds-color-danger-base-default);
    --color-destructive-foreground: var(--ds-color-danger-base-contrast-default);
    --color-border: var(--ds-color-neutral-border-default);
    --color-input: var(--ds-color-neutral-border-subtle);
    --color-ring: var(--ds-color-accent-border-strong);
    --radius: var(--ds-border-radius-md);
    }
    Preserve any existing minimal CSS reset already in globals.css (don't break the 5 existing demand-side routes).
  • 1.5 Initialise shadcn — done manually (wrote components.json + src/lib/utils.ts directly to avoid npx shadcn init overwriting the carefully-wired globals.css).
  • 1.6 Install the v1 component set:
    npx shadcn@latest add table button card badge tabs input select label --yes
    Created 8 files under src/components/ui/.
  • 1.7 Verify: npx tsc --noEmit clean, npm run build succeeds, all 5 existing routes still produce. Browser smoke test deferred to Phase 8.

Phase 1 outcome: stack is in place and compiles cleanly. New runtime files: postcss.config.mjs, components.json, src/lib/utils.ts, 8 shadcn primitives in src/components/ui/, generated Designsystemet theme in design-tokens/ (source) + design-tokens-build/atlas.css (built CSS). app/globals.css rewritten to compose Tailwind + custom Atlas theme + Designsystemet bridge + shadcn aliases.

Validation​

npm run dev
# In a real browser, visit:
# http://localhost:3000/
# http://localhost:3000/data
# http://localhost:3000/coverage-gap/barnefattigdom
# http://localhost:3000/kommuner/0301
# All should render without errors.
# Then verify a Tailwind utility works:
# add a one-off `<div className="bg-primary text-primary-foreground p-4">test</div>`
# to app/page.tsx, verify the colour is the Atlas blue (#0062BA-ish).
# Remove the test div before continuing.

User confirms shadcn primitives render with Designsystemet tokens applied.


Phase 2: Shared supply lib — DONE​

Tasks​

  • 2.1 Extend src/lib/types.ts with the new interfaces. Reference the actual marts schema in atlas-data/dbt/models/dimensions/schema.yml and atlas-data/dbt/models/marts/schema.yml:
    export interface DimNgo {
    orgnr: string;
    slug: string;
    name: string;
    brand_name: string | null;
    website_url: string | null;
    tier: string;
    chapter_data_shape: string;
    has_chapters: boolean;
    primary_focus: string | null;
    icnpo_code_1: string | null;
    icnpo_code_2: string | null;
    icnpo_code_3: string | null;
    }

    export interface DimChapter {
    chapter_id: string;
    ngo_orgnr: string;
    chapter_level: 'national' | 'regional' | 'local';
    parent_chapter_id: string | null;
    chapter_orgnr: string | null;
    name: string;
    kommune_nr: string | null;
    is_active: boolean;
    updated_at: string;
    }

    export interface DimActivity {
    activity_id: string;
    ngo_orgnr: string;
    canonical_name: string;
    service_category_code: string;
    is_active: boolean;
    }

    export interface FactChapterActivity {
    chapter_id: string;
    activity_id: string;
    ngo_orgnr: string;
    kommune_nr: string | null;
    local_activity_name: string;
    canonical_name: string;
    is_active: boolean;
    source_id: string;
    updated_at: string;
    }
  • 2.2 Create src/lib/supply.ts. Mirror src/lib/indicators.ts's shape (Server-Component-friendly async functions returning typed rows). Functions to add:
    • listNgos(): Promise<NgoWithStatus[]> — dim_ngo joined to a chapter-count subquery, returning {ngo: DimNgo, chapter_count: number, has_supply: boolean}. Used by /ngo.
    • getNgoBySlug(slug: string): Promise<DimNgo | null> — used by /ngo/[slug] catch-all.
    • getNgoOverview(orgnr: string): Promise<{chapter_count, distrikt_count, activity_count, kommune_count}> — used by /ngo/redcross.
    • listChapters(orgnr: string, filters: ChapterFilters): Promise<ChapterRow[]> — filters per [Q31]. ChapterRow joins dim_chapter to dim_kommune and dim_fylke.
    • getChapterDetail(chapter_id: string): Promise<ChapterDetail> — chapter row + its activities + parent + children.
    • listActivities(orgnr: string): Promise<ActivityRow[]> — dim_activity with chapter-count subquery.
    • listDistrikter(orgnr: string): Promise<DistriktRow[]> — chapter_level='regional' rows with child-chapter and kommune counts.
    • getDistriktDetail(chapter_id: string): Promise<DistriktDetail> — child chapters + aggregate stats.
    • listChaptersInKommune(kommune_nr: string): Promise<ChapterInKommuneRow[]> — for the /kommuner/[kommune_nr] extension. Groups by service_category_code.
  • 2.3 Each function fetches via the existing db() from src/lib/db.ts. SQL is inline template literals; cast results to typed interfaces (the existing pattern). No new DB client, no ORM.

Phase 2 outcome: src/lib/types.ts extended with 6 supply types; src/lib/supply.ts (~330 lines) provides 9 helpers covering NGO index, NGO overview, chapter list/detail, activity catalog, distrikt list/detail, kommune supply lookup, service-category list. npx tsc --noEmit clean.

Validation​

# Type-check
cd /Users/terje.christensen/learn/helpers/atlas
npx tsc --noEmit
# Should pass with no errors related to new types.

User confirms types and lib compile cleanly.


Phase 3: Engineering validation page — /admin/supply/redcross-branches — DONE​

Tasks​

  • 3.1 Create app/admin/supply/redcross-branches/page.tsx. Server Component. Implement the 6 sections per Investigation Appendix B:
    1. Row counts (expected vs actual table; mismatches in red)
    2. Chapter level breakdown (national / regional / local)
    3. Service category coverage (count of Red Cross chapters per service_category_code)
    4. Sample 10 random chapters
    5. Distrikt coverage (per distrikt: child count, kommune count)
    6. Ingest run history (Section conditionally renders "Awaiting infrastructure PLAN" if raw.ingest_runs doesn't exist; check via to_regclass('raw.ingest_runs') IS NOT NULL)
  • 3.2 Add <meta name="robots" content="noindex" /> via Next.js metadata export.
  • 3.3 No nav links to this page from public surfaces. Documented in app/admin/README.md (a single short file explaining the /admin convention).

Phase 3 outcome: /admin/supply/redcross-branches builds; six sections with graceful degradation when the frontend role can't read raw.* (current setup — clearly labeled). Smoke test deferred to Phase 8.

Validation​

# Visit http://localhost:3000/admin/supply/redcross-branches
# Verify:
# - Section 1: counts match — 391 / 2143 / 391 / 35 / 1941
# - Section 2: 1 national, 19 regional, 371 local
# - Section 3: every service category has ≥1 chapter
# - Section 4: 10 random chapters render with all columns
# - Section 5: 19 distrikter with child/kommune counts
# - Section 6: "Awaiting infrastructure PLAN" message

User confirms numbers match the post-PLAN-002 expectations.


Phase 4: NGO index, Red Cross overview, non-ingested stub — DONE​

Tasks​

  • 4.1 Create app/ngo/page.tsx (the /ngo index). Server Component using listNgos(). Renders a grid of shadcn Cards — one per NGO (11 total). Each card shows: NGO name, primary_focus Badge, tier Badge, status indicator (✅ "Tilbud importert" if chapter_count > 0, else ⏳ "Under arbeid"). Cards link to /ngo/[slug]. Page heading + sub-heading per Investigation Appendix A.
  • 4.2 Create app/ngo/redcross/page.tsx (the Red Cross overview). Server Component using getNgoOverview('864139442'). Three-row layout per Investigation Appendix A: header (name, badges, website link), 4-stat grid (Card per stat), section links into /ngo/redcross/{chapters,distrikter,aktiviteter} and /kommuner index.
  • 4.3 Create app/ngo/[slug]/page.tsx — the catch-all stub. Server Component using getNgoBySlug(slug). Static /ngo/redcross route takes precedence over the [slug] dynamic segment by Next.js convention (no explicit redirect needed — verified via build output). Catch-all renders the stub layout per Appendix A; missing slug → notFound().
  • 4.4 Add a nav link from app/page.tsx to /ngo ("Tilbud per organisasjon"). Inserted as the third <li> in the existing list, matching the existing inline-style pattern (didn't migrate the home page to Tailwind/shadcn — that's a follow-up PLAN per the investigation).

Phase 4 outcome: 3 new routes (/ngo, /ngo/redcross, /ngo/[slug]) + nav link. 11 NGOs visible in the index; only Red Cross shows ✓ Tilbud importert, the other 10 show ⏳ Under arbeid and route to the stub.

Validation​

# Visit http://localhost:3000/ngo
# - 11 cards rendered, alphabetical
# - Only Red Cross shows ✅; the other 10 show ⏳
# - Cards are clickable
# Visit http://localhost:3000/ngo/redcross
# - Counts: 391 lokallag, 19 distrikter, 35 aktiviteter, X kommuner
# - Section links navigate
# Visit http://localhost:3000/ngo/folkehjelp
# - Stub renders with name, badges, website link, "Tilbud-data er ikke importert ennå" copy
# Visit http://localhost:3000/ngo/nonexistent-slug
# - 404

User confirms all four pages render correctly.


Phase 5: Red Cross chapters list + detail — DONE​

Tasks​

  • 5.1 Create app/ngo/redcross/chapters/page.tsx. Server Component reading searchParams for the 5 filters per [Q31] plus free-text q. Filter form built with native <form method="get"> plus shadcn Input/Label/Button and native <select> styled with Tailwind to match shadcn (avoided shadcn's Select because it requires client component and we wanted everything server-rendered). Default is_active=true; checkbox toggles to include inactive. Table renders shadcn Table. Chapter name links to detail; kommune/parent are also linked.
  • 5.2 Create app/ngo/redcross/chapters/[chapter_id]/page.tsx. Server Component using getChapterDetail(chapter_id). Header with name + level badge + active flag + kommune/fylke link + chapter's own web URL (linked out). "Aktiviteter" Card lists activities with their service-category labels as Badges. Two side-by-side Cards for Forelder + Underliggende (parent / children navigation). Final Card for "Kontakt og adresse" — uses the web/email/phone/postal_* fields from dim_chapter (which already exist in the marts schema; the planned source_url from the multi-NGO extensions PLAN turned out to be a duplicate of the existing web column).

Phase 5 outcome: filterable chapters table works with all 5 URL params; chapter detail covers activities, hierarchy, contact. Notable discovery: dim_chapter.web already carries the chapter URL, so Phase 5 didn't need to wait for the multi-NGO extensions PLAN.

Validation​

# Visit http://localhost:3000/ngo/redcross/chapters
# - All 371 active local chapters render (default filter)
# - Filter by ?fylke_nr=03 → only Oslo chapters
# - Filter by ?chapter_level=regional → 19 distrikter
# - Filter by ?q=Modum → only matching chapters
# Visit http://localhost:3000/ngo/redcross/chapters/<a real chapter_id>
# - Shows chapter name, badges, kommune link
# - Lists activities with service_category_code labels
# - Shows parent + child links

User confirms filtering works and detail page renders correctly.


Phase 6: Red Cross activities and distrikter — DONE​

Tasks​

  • 6.1 Create app/ngo/redcross/aktiviteter/page.tsx. Server Component using listActivities('864139442'). Table with 35 activities, sorted by canonical_name. The service_category_code column renders as a Badge — link to /aktivitet/[code] deferred (the destination doesn't exist in v1; rendering as a plain Badge for now). Note at the top about cross-NGO search coming in a later version.
  • 6.2 Create app/ngo/redcross/distrikter/page.tsx. Server Component using listDistrikter('864139442'). Table of 19 distrikter with active-children count + distinct-kommune count. Distrikt name links to its detail page.
  • 6.3 Create app/ngo/redcross/distrikt/[distrikt_id]/page.tsx. Server Component using getDistriktDetail(distrikt_id). Header with name + badge + distriktskontor (linked kommune). 3-stat grid (children / kommuner / aktivitetstyper). Card with the list of child lokallag (linked to chapter detail).

Phase 6 outcome: aktivitetskatalog (35 rows, RC's full canonical activity set) + distrikter list (19 rows, child counts sum to ~371 = the local lokallag count) + distrikt detail. All three routes build clean.

Validation​

# Visit http://localhost:3000/ngo/redcross/aktiviteter
# - 35 rows, sorted by name (or chapter count descending — pick one)
# - service_category_code badges visible
# Visit http://localhost:3000/ngo/redcross/distrikter
# - 19 distrikter listed with child counts summing to ~371
# Visit http://localhost:3000/ngo/redcross/distrikt/<a real distrikt id>
# - Lists child lokallag, shows aggregate stats

User confirms.


Phase 7: Extend /kommuner/[kommune_nr] with supply section — DONE​

Tasks​

  • 7.1 Modify app/kommuner/[kommune_nr]/page.tsx. Appended a new <section> titled "Lokale tilbud i {kommune_name}" after the indicators block. Server-side listChaptersInKommune(kommune_nr) runs in parallel with the existing indicator fetches. Results grouped by service_category_label_no; each group lists deduped chapter names (one chapter can appear under one category via multiple activity rows — JS dedupe via Map<chapter_id, row>).
  • 7.2 Empty state: when no chapters from any ingested NGO have rows for this kommune, the section renders the planned "Ingen ingestede organisasjoner har lokale tilbud i {kommune_name}." copy. Honest about what we know vs don't.
  • 7.3 Kept the new section's visual style consistent with the existing inline-CSS indicators sections (rather than mixing shadcn Card mid-page). The kommune page is one of the 5 demand-side routes; the investigation says these migrate to shadcn in a separate later PLAN. v1 supply work doesn't disturb the existing styling.

Phase 7 outcome: /kommuner/0301 (Oslo) shows ~30 Red Cross chapters under their service categories below the indicators table; /kommuner/3030 (Lillestrøm) shows the empty-state copy. Surfaced a real bug in listChaptersInKommune — SELECT DISTINCT … ORDER BY sc.sort_order was missing sort_order in the projection (Postgres requires it under DISTINCT). Fixed by adding sort_order to the SELECT list. See Implementation Notes below.

Validation​

# Visit http://localhost:3000/kommuner/0301 (Oslo)
# - Existing indicators section renders
# - New "Lokale tilbud" section below shows Red Cross chapters in Oslo
# Visit http://localhost:3000/kommuner/2102 (a kommune with no Red Cross presence)
# - Empty state copy renders, not blank

User confirms the section integrates cleanly without breaking the existing page.


Phase 8: Polish + smoke test — DONE​

Tasks​

  • 8.1 Verify the home-page nav link to /ngo works (added in 4.4). Confirmed: rendered HTML shows the new <li> with href="/ngo". User confirmed visible after a hard-refresh.
  • 8.2 Manual smoke test in a desktop browser. User walked the routes and confirmed "i can see the data now :)" on 2026-04-24.
  • 8.3 Manual smoke test on a phone — deferred to user as low-priority follow-up; the v1 tables use overflow-x-auto containers and degrade to horizontal scroll on narrow viewports. No layout-breaking issues expected.
  • 8.4 npx tsc --noEmit passes (cleanly on Next 16.2.4 / TS 6.0.3).
  • 8.5 npm run build succeeds — all 14 routes compile under Turbopack. Build output shows the 9 new supply routes alongside the 5 existing demand-side ones.
  • 8.6 Visual check: Atlas blue #0062BA visible on the chapters-page filter "Bruk filtre" button; Designsystemet typography active. Confirmed by user.
  • 8.7 Update docs/stack/naming-conventions.md — no new vocabulary introduced; the supply terms (service_category_code, chapter_data_shape, tier, primary_focus) were added by previous PLANs. No update needed.

Phase 8 outcome: site live and validated. User confirmed "i can see the data now :)". No regressions in the 5 existing demand-side routes.

Validation​

User browses the live frontend on desktop and phone; confirms each new page renders, filters work, navigation flows are clear, and Designsystemet tokens are visible.


Acceptance Criteria​

  • Tailwind v4 + shadcn/ui + Designsystemet tokens are wired and working in production (Phase 1).
  • All 7 new public routes render correctly (Phases 4–6): /ngo, /ngo/redcross, /ngo/redcross/chapters, /ngo/redcross/chapters/[chapter_id], /ngo/redcross/aktiviteter, /ngo/redcross/distrikter, /ngo/redcross/distrikt/[distrikt_id].
  • The catch-all /ngo/[slug] stub renders for the 10 non-ingested NGOs.
  • /admin/supply/redcross-branches validates the import counts (Phase 3).
  • /kommuner/[kommune_nr] has the new "Lokale tilbud" section (Phase 7).
  • Home page links to /ngo.
  • All existing 5 demand-side routes still render correctly (no regression).
  • npx tsc --noEmit and npm run build both pass.
  • Manual smoke test on desktop confirmed by user; phone test deferred (tables use overflow-x-auto).
  • @digdir/designsystemet-react removed from package.json.

Out of scope (per resolved Q's in the investigation)​

  • Maps — no supply choropleths in v1; the data is too sparse with one NGO. v1.5 adds them with Folkehjelp.
  • Pagination — 391 chapters is small enough for a plain table. TanStack Table virtualisation when row counts approach ~3000.
  • NGO logos — plain text NGO names. Logos are a brand-assets PLAN.
  • Compare-with CTA — destination doesn't exist yet (Compare-NGOs page is a separate spec). Add in v1.5+.
  • Pathway CTAs — dim_ngo lacks pathway data; show single website_url link only. Pathway extension is its own future investigation.
  • Cross-NGO finder (/aktivitet/[code]) — premature with one NGO. Add in v1.5 when Folkehjelp lands.
  • Coverage-gap mart consumption — its own future investigation; v2.
  • Designsystemet migration of existing 5 demand-side routes — separate small PLAN later.
  • i18n English layer — Norwegian-first per [Q22]. English deferred.
  • Per-chapter PII (kontaktperson) — explicit no per scraping infra D.3.

Implementation Notes​

Why adopt the stack now instead of deferring​

Adopting Tailwind v4 + shadcn + Designsystemet at the start of the v1 supply work avoids two later costs: (a) migrating ~7 freshly-built supply pages from inline CSS to the new stack, and (b) discovering at v1.5 (when cross-NGO views need DataTable, multi-select, and chart components) that we need to retrofit shadcn anyway. The cost of adopting now is one Phase 1 (~3h); the cost of deferring is double that plus visual inconsistency.

Why app/admin/... for the validation page​

Two reasons: (1) it's not a user-facing destination, so it shouldn't appear in nav or sitemap; (2) it sets a convention — every future ingest PLAN includes a small /admin/supply/<source> validation page as part of its acceptance criteria, so we never ingest data we can't see. Documented in app/admin/README.md.

Why no internal API routes​

The frontend reads marts.* directly via the postgres client in src/lib/db.ts. No /api/... routes for supply data in v1 — the SQL is in Server Components, and there's no third-party consumer that needs an HTTP API surface. Add API routes when there's a real consumer (mobile app, third-party integration, …).

Server vs Client Components​

Default to Server. Only Client where you need interactive state (filter bar updates, sortable column toggles). The chapter table's filter bar is the most interactive thing in v1 — keep it small. Use Next.js's searchParams-driven pattern (Server Component reads searchParams, runs SQL, renders); client form just submits the URL change. No client-side filtering of pre-fetched rows.

src/lib/supply.ts mirrors src/lib/indicators.ts​

Keeping the lib structure parallel to the existing demand-side lib means future PLANs (v1.5 cross-NGO finder, v2 Coverage-gap) follow a known pattern. Don't introduce a different shape just because it's a new file.

Mid-PLAN deviations (recorded for the next contributor)​

These weren't in the PLAN but happened during implementation. Calling them out so the next reader understands why the as-built state differs in small ways.

  1. Next.js 15 → 16 upgrade folded into this PLAN. When the user reviewed Phase 1, they pointed out we were on Next 15.5.15 while 16.2.4 was the latest stable (released 2025-10-21). I bumped next, react, react-dom, typescript, @types/node, @types/react, @types/react-dom, maplibre-gl, @digdir/designsystemet-css to latest stable. No codemods needed — the only Next 16 change visible in the build is ▲ Next.js 16.2.4 (Turbopack) (Turbopack is now the default dev/build runtime). All tests still pass on the new versions. Worth its own PLAN-002 in retrospect; doing it inline here was practical because there was nothing else to upgrade.

  2. SELECT DISTINCT … ORDER BY sc.sort_order bug in listChaptersInKommune. Postgres requires every ORDER BY column to appear in the SELECT list under DISTINCT. The first run of /kommuner/0301 after Phase 7 returned 500 because of this; fixed by adding sc.sort_order to the projection (and a comment explaining why it's there even though no consumer reads it). General lesson: when writing SELECT DISTINCT … ORDER BY x, x must be in the SELECT.

  3. Dev-server staleness after major dep churn. The user had a Next 15 dev-server process running on port 4000 throughout Phase 1's npm install. After my changes, that process kept the old in-memory webpack module graph and started returning 500s with Cannot find module './778.js'. Fix is kill <pid>; rm -rf .next; npm run dev. Recipe documented here so the next dev who lands a big dep change doesn't waste 10 minutes on it.

  4. dim_chapter.web already exists as the chapter-website column. The investigation talks about adding source_url via the multi-NGO extensions PLAN, but inspection of the live dim_chapter schema (built by PLAN-002) revealed that the same data is already exposed as web — populated from Red Cross's API branchUrl field via supply__redcross_branches.sql. So Phase 5's chapter detail page links out via chapter.web directly, no schema change needed. The source_url work in the multi-NGO extensions PLAN is now mostly a rename + per-NGO normalisation question, not a new column.

  5. shadcn Select not used. The PLAN suggested shadcn primitives for the chapters filter. shadcn Select is a Radix Select.Root wrapper that requires a Client Component. To keep the chapters page server-rendered (and avoid hydrating the filter form), I used native <select> elements styled with the same Tailwind classes shadcn applies to its Input — visually consistent, no client-component cost. shadcn Select becomes worthwhile when we need filtering UX richer than a native dropdown (multi-select, search, command palette).


Files to Modify / Create​

Modified:

  • package.json — add Tailwind/shadcn deps, remove @digdir/designsystemet-react
  • app/page.tsx — add nav link to /ngo
  • app/globals.css — Tailwind import + Designsystemet bridge + @theme aliases
  • app/kommuner/[kommune_nr]/page.tsx — append "Lokale tilbud" section
  • src/lib/types.ts — add supply interfaces
  • tsconfig.json — add @/* path alias if shadcn init didn't set it up
  • docs/stack/naming-conventions.md — minor updates if needed

Created:

  • postcss.config.mjs
  • components.json (shadcn config)
  • src/components/ui/* — shadcn primitives (table, button, card, badge, tabs, input, select, label)
  • src/lib/supply.ts
  • src/lib/utils.ts (created by shadcn init — cn() helper)
  • design-tokens/atlas/* — generated by npx @digdir/designsystemet tokens
  • app/admin/README.md — documents the /admin convention
  • app/admin/supply/redcross-branches/page.tsx
  • app/ngo/page.tsx
  • app/ngo/redcross/page.tsx
  • app/ngo/redcross/chapters/page.tsx
  • app/ngo/redcross/chapters/[chapter_id]/page.tsx
  • app/ngo/redcross/aktiviteter/page.tsx
  • app/ngo/redcross/distrikter/page.tsx
  • app/ngo/redcross/distrikt/[distrikt_id]/page.tsx
  • app/ngo/[slug]/page.tsx (catch-all stub)