feat: core layout, UI atoms, homepage + behaviour modules

- UI atoms: Chip, Button, SplitCvButton, LangSwitch, ThemeToggle, Card,
  SectionLabel, TraceMotif, FramedImage
- core: Base/Seo/Header/MobileNav/Footer/HintBar/SkipLink/ThemeScript
- homepage sections: Hero, ProofStrip, SkillsGrid, ProjectCards,
  ExperienceTimeline, AboutTeaser, ContactBand + Homepage composition (EN/NO)
- behaviour modules: theme, nav (overlay/hint/copy/header), observer (reveal/spy),
  lightbox, form — progressive enhancement, fail-silent
- SEO head component: meta, hreflang from slug map, JSON-LD, OG references
- move content data to src/data (avoid Astro reserved content-collection folder)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-07-04 01:20:46 +02:00
parent 2184a1892f
commit 1fce1b3cec
45 changed files with 1891 additions and 27 deletions
+324
View File
@@ -0,0 +1,324 @@
import type { Project } from '@lib/schema';
/* InboxIntel case study. Source: F:\Documents\InboxIntel\InboxIntel (README + docs). */
export const inboxintel: Project = {
id: 'inboxintel',
name: 'InboxIntel',
order: 2,
status: 'in-development',
template: 'case-study',
stack: [
{ name: '.NET 8', context: { en: 'ASP.NET Core', no: 'ASP.NET Core' } },
{ name: 'PostgreSQL', context: { en: 'EF Core + Npgsql', no: 'EF Core + Npgsql' } },
{ name: 'React 18', context: { en: 'Vite · Chart.js', no: 'Vite · Chart.js' } },
{ name: 'Gmail API', context: { en: 'OAuth2 · Polly', no: 'OAuth2 · Polly' } },
{ name: 'Serilog' },
{ name: 'Docker' },
],
links: [],
diagram: {
viewBox: '0 0 1120 300',
title: { en: 'InboxIntel architecture', no: 'InboxIntel-arkitektur' },
desc: {
en: 'A React SPA calls an ASP.NET Core API layered as Clean Architecture (Api → Infrastructure → Application → Domain). A hosted background worker syncs the external Gmail API into PostgreSQL with Polly retry/backoff.',
no: 'En React-app kaller et ASP.NET Core-API bygget som Clean Architecture (Api → Infrastructure → Application → Domain). En bakgrunnstjeneste synkroniserer det eksterne Gmail-API-et inn i PostgreSQL med Polly retry/backoff.',
},
nodes: [
{
id: 'spa',
x: 40,
y: 116,
w: 170,
h: 72,
kind: 'internal',
label: 'React SPA',
sub: { en: 'Vite · Chart.js', no: 'Vite · Chart.js' },
},
{
id: 'api',
x: 290,
y: 116,
w: 200,
h: 72,
kind: 'primary',
label: 'ASP.NET Core API',
sub: { en: 'Clean Architecture', no: 'Clean Architecture' },
},
{
id: 'pg',
x: 580,
y: 48,
w: 180,
h: 64,
kind: 'internal',
label: 'PostgreSQL',
sub: { en: 'EF Core · Npgsql', no: 'EF Core · Npgsql' },
},
{
id: 'worker',
x: 580,
y: 160,
w: 180,
h: 64,
kind: 'internal',
label: 'Sync worker',
sub: { en: 'hosted · Polly', no: 'hostet · Polly' },
},
{
id: 'gmail',
x: 900,
y: 152,
w: 180,
h: 72,
kind: 'external',
label: 'Gmail API',
sub: { en: 'external · OAuth2', no: 'eksternt · OAuth2' },
},
],
edges: [
{ d: 'M210 152 H290', kind: 'flow', label: '/api', labelX: 240, labelY: 144 },
{ d: 'M490 134 L580 92', kind: 'flow' },
{ d: 'M490 170 L580 192', kind: 'flow' },
{ d: 'M670 160 V112', kind: 'flow' },
{ d: 'M760 192 H900', kind: 'external', label: 'sync', labelX: 820, labelY: 184 },
],
},
media: [
{
src: 'placeholder:inboxintel-dashboard',
width: 1600,
height: 1000,
alt: {
en: 'InboxIntel draggable analytics dashboard with charts',
no: 'InboxIntel dashbord med flyttbare analyse-widgets',
},
caption: {
en: 'Draggable analytics dashboard (react-grid-layout)',
no: 'Dashbord med flyttbare widgets (react-grid-layout)',
},
},
{
src: 'placeholder:inboxintel-cleanup',
width: 1600,
height: 1000,
alt: {
en: 'Bulk cleanup preview screen requiring explicit confirmation',
no: 'Forhåndsvisning av masseopprydding som krever bekreftelse',
},
caption: {
en: 'Cleanup preview — nothing is deleted without a confirmed flag',
no: 'Oppryddings-forhåndsvisning — ingenting slettes uten bekreftelse',
},
},
],
content: {
en: {
valueProp: 'Gmail analytics and safe bulk cleanup, built as a disciplined Clean Architecture.',
cardTeaser:
'Gmail analytics and safe bulk cleanup — Clean Architecture, encrypted OAuth tokens, preview-before-delete by design.',
tldr: {
what: 'Gmail analytics, cleanup and unsubscribe management with a background sync worker.',
why: 'To practise Clean Architecture properly and make destructive operations safe by design.',
stack: '.NET 8 · PostgreSQL · React 18 · Gmail API · Serilog · Polly · Docker.',
role: 'Sole architect and developer, front to back.',
},
sections: [
{
kind: 'problem',
heading: 'Problem & context',
anchorId: 'problem',
blocks: [
{
type: 'p',
text: 'A busy inbox needs analytics and cleanup, but bulk operations on real mail are dangerous — one wrong filter can delete things you cant get back. I built InboxIntel to explore two things at once: a clean, testable backend architecture, and destructive operations that are safe by construction.',
},
],
},
{
kind: 'architecture',
heading: 'Architecture',
anchorId: 'architecture',
blocks: [
{
type: 'p',
text: 'A four-project Clean Architecture solution behind a React SPA. The dependency rule points inward: Api → Infrastructure → Application → Domain. Controllers hold no business logic — they delegate to Application-layer service interfaces resolved through DI. A hosted background worker syncs Gmail into PostgreSQL with Polly retry and backoff.',
},
{
type: 'ul',
items: [
'Domain: entities and enums, no external dependencies.',
'Application: service interfaces, DTOs, validators, the Gmail query parser.',
'Infrastructure: EF Core, the Gmail client, the sync worker, AI and export.',
'Api: ASP.NET Core controllers, auth, DI and Serilog — thin by design.',
],
},
],
},
{
kind: 'decisions',
heading: 'Key decisions & trade-offs',
anchorId: 'decisions',
decisions: [
{
n: 1,
choice: 'Full Clean Architecture across four projects.',
alternative: 'A single project would have shipped faster.',
rationale:
'I wanted the dependency rule to be enforced by the project structure, not by discipline alone. It costs more ceremony up front and pays back in testability and clear boundaries.',
},
{
n: 2,
choice: 'Encrypt OAuth refresh tokens at rest and never log them.',
alternative: 'Store them as plain columns.',
rationale:
'Refresh tokens are long-lived keys to someones mailbox. Theyre encrypted with the ASP.NET Core Data Protection API (AES), with keys persisted to a mounted volume — the single most important security decision in the app.',
},
{
n: 3,
choice: 'Every destructive action is preview-then-confirm.',
alternative: 'Delete immediately on request.',
rationale:
'All cleanup and unsubscribe actions require a server-side preview and an explicit Confirmed flag. The AI layer is advisory only and can never trigger a deletion.',
},
],
},
{
kind: 'security',
heading: 'Security & production notes',
anchorId: 'security',
blocks: [
{
type: 'ul',
items: [
'Gmail scopes are read/modify only — no send scope is ever requested.',
'Polly provides retry and backoff against Gmail API rate limits and transient failures.',
'FluentValidation on inputs; Serilog structured logging that never records tokens.',
'Unit tests cover the query parser and unsubscribe extraction; integration tests boot the API host and assert authorization is enforced.',
],
},
],
},
{
kind: 'screenshots',
heading: 'Screenshots',
anchorId: 'screenshots',
},
{
kind: 'next',
heading: 'Status & whats next',
anchorId: 'next',
blocks: [
{
type: 'p',
text: 'InboxIntel is in active development: the architecture is complete and the layers compile and are wired end to end, with a few integration points (Gmail parsing edge cases, AI prompt tuning) left as clearly-marked extension points. Next up is hardening those edges and expanding the integration-test suite.',
},
],
},
],
},
no: {
valueProp: 'Gmail-analyse og trygg masseopprydding, bygget som en ryddig Clean Architecture.',
cardTeaser:
'Gmail-analyse og trygg masseopprydding — Clean Architecture, krypterte OAuth-tokens, forhåndsvisning før sletting.',
tldr: {
what: 'Gmail-analyse, opprydding og avmeldingshåndtering med en bakgrunns-synk.',
why: 'For å øve på Clean Architecture skikkelig og gjøre destruktive operasjoner trygge fra bunnen.',
stack: '.NET 8 · PostgreSQL · React 18 · Gmail API · Serilog · Polly · Docker.',
role: 'Eneste arkitekt og utvikler, fra ende til ende.',
},
sections: [
{
kind: 'problem',
heading: 'Problem og kontekst',
anchorId: 'problem',
blocks: [
{
type: 'p',
text: 'En travel innboks trenger analyse og opprydding, men masseoperasjoner på ekte e-post er farlige — ett feil filter kan slette ting du ikke får tilbake. Jeg bygde InboxIntel for å utforske to ting samtidig: en ryddig, testbar backend-arkitektur, og destruktive operasjoner som er trygge fra bunnen.',
},
],
},
{
kind: 'architecture',
heading: 'Arkitektur',
anchorId: 'arkitektur',
blocks: [
{
type: 'p',
text: 'En løsning med fire prosjekter i Clean Architecture bak en React-app. Avhengighetsregelen peker innover: Api → Infrastructure → Application → Domain. Kontrollerne har ingen forretningslogikk — de delegerer til tjenestegrensesnitt i Application-laget via DI. En bakgrunnstjeneste synkroniserer Gmail inn i PostgreSQL med Polly retry og backoff.',
},
{
type: 'ul',
items: [
'Domain: entiteter og enums, uten eksterne avhengigheter.',
'Application: tjenestegrensesnitt, DTO-er, validatorer og Gmail-søkeparser.',
'Infrastructure: EF Core, Gmail-klient, synk-tjeneste, AI og eksport.',
'Api: ASP.NET Core-kontrollere, auth, DI og Serilog — tynt med hensikt.',
],
},
],
},
{
kind: 'decisions',
heading: 'Viktige valg og avveininger',
anchorId: 'beslutninger',
decisions: [
{
n: 1,
choice: 'Full Clean Architecture over fire prosjekter.',
alternative: 'Ett prosjekt hadde vært raskere å levere.',
rationale:
'Jeg ville at avhengighetsregelen skulle håndheves av prosjektstrukturen, ikke bare av disiplin. Det koster mer seremoni i starten og betaler seg i testbarhet og tydelige grenser.',
},
{
n: 2,
choice: 'Kryptere OAuth-refresh-tokens i ro og aldri logge dem.',
alternative: 'Lagre dem som vanlige kolonner.',
rationale:
'Refresh-tokens er langlevde nøkler til noens innboks. De krypteres med ASP.NET Core Data Protection API (AES), med nøkler lagret på et montert volum — det viktigste sikkerhetsvalget i appen.',
},
{
n: 3,
choice: 'Alle destruktive handlinger er forhåndsvis-så-bekreft.',
alternative: 'Slette umiddelbart ved forespørsel.',
rationale:
'All opprydding og avmelding krever en server-side forhåndsvisning og et eksplisitt Confirmed-flagg. AI-laget er kun rådgivende og kan aldri utløse en sletting.',
},
],
},
{
kind: 'security',
heading: 'Sikkerhet og drift',
anchorId: 'sikkerhet',
blocks: [
{
type: 'ul',
items: [
'Gmail-scopes er kun lese/endre — send-scope blir aldri etterspurt.',
'Polly gir retry og backoff mot Gmail-API-ets rategrenser og forbigående feil.',
'FluentValidation på input; strukturert Serilog-logging som aldri lagrer tokens.',
'Enhetstester dekker søkeparser og avmeldings-uttrekk; integrasjonstester starter API-verten og sjekker at autorisasjon håndheves.',
],
},
],
},
{
kind: 'screenshots',
heading: 'Skjermbilder',
anchorId: 'skjermbilder',
},
{
kind: 'next',
heading: 'Status og veien videre',
anchorId: 'videre',
blocks: [
{
type: 'p',
text: 'InboxIntel er under aktiv utvikling: arkitekturen er komplett, lagene kompilerer og er koblet ende til ende, med noen integrasjonspunkter (kanttilfeller i Gmail-parsing, tuning av AI-prompter) igjen som tydelig markerte utvidelsespunkter. Neste steg er å herde disse kantene og utvide integrasjonstestene.',
},
],
},
],
},
},
};