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:
@@ -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 can’t 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 someone’s mailbox. They’re 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 & what’s 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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user