Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# personal-website — Agent Guide

Dom Mangonon's single-page portfolio, deployed to https://dommango.github.io. Accessibility
guidelines: `ACCESSIBILITY.md`.

## Stack

Next.js 16 App Router · React 19 · TS strict · Tailwind 4 · static export (no server runtime,
all data loaded at build time) · GitHub Pages deploy via `.github/workflows/deploy.yml`.

## Commands

- `npm install --legacy-peer-deps` — react-simple-maps declares a React <19 peer; CI uses it too
- `npm run dev` / `build` (→ `out/`) / `lint` · `npx tsc --noEmit`
- `npm test -- --run` (vitest; drop `-- --run` for watch mode) · `npx playwright test` (e2e)

## Hard rules

- **Never commit Citi/employer-confidential content.** History was purged once already
(2026-08-28: filter-rewrite + force-push across `main`/feature branches) after regulator-
confidential material (Consent Order text, MRA reporting detail) leaked into commits on this
public repo. Career content is gitignored by design — source of record is `~/personal/career`,
not this repo; don't re-add it or reintroduce a sync step. `'Citi · SVP, Transformation'`
(LinkedIn-grade) is fine; verbatim regulatory/internal detail is not. A dangling old SHA
(`cf65730`) may still be GitHub-support-GC pending — don't assume force-push alone is enough.
- Four places must stay in sync when adding/changing a landing section, or the scroll-spy breaks
silently: the component in `components/landing/`, the `SectionId` union in `Nav.tsx`,
`SECTION_IDS` in `BrutalistLanding.tsx` (must match DOM order — spy takes the last element with
`offsetTop <= scrollY`), and the `link()` calls in `Nav.tsx`. A conditional section (e.g.
Writing) needs its nav link gated by the same predicate as the section.
- Build sections from semantic tokens only (`var(--accent)`, `var(--fg)`, `var(--fg-muted)`,
`var(--rule)`, `var(--s-N)`, `var(--font-*)`) in `app/globals.css` — that's what makes all three
themes (Gold/Oxblood/High Contrast) work via `[data-accent]`/`[data-contrast]`. The landing
(`.brutalist-root`) is dark-only by design and shadows the legacy `:root` light block. One
responsive breakpoint, `@media (max-width: 900px)` at the bottom of globals.css — new grids
aren't automatic. Structure convention: `.section` > `<BinaryRule/>` > `.*-head`, order matters
(adjacent-sibling rule supplies top margin). `BinaryRule`'s `seed` prop drives a deterministic
PRNG for hydration safety, not aesthetics — give new sections an unused seed.
- Content is hand-authored TS in `lib/content/`, not markdown/JSON (`resolveJsonModule` +
`strict` infers `never[]` from an empty JSON array, which fails typecheck). Travel data is the
exception — script-generated JSON via `scripts/process-travel-data.js` /
`scripts/fetch-flights.js` (both manual, not automated).
- `github-actions[bot]` pushes with the default `GITHUB_TOKEN` don't trigger workflows, so a cron
that commits data can't make the site rebuild. Anything needing fresh data at deploy time must
fetch during the build, not commit-then-rebuild.

## Env (`.env.local`, copy from `.env.example`)

EmailJS (`NEXT_PUBLIC_EMAILJS_SERVICE_ID`/`_TEMPLATE_ID`/`_PUBLIC_KEY`,
`NEXT_PUBLIC_EMAILJS_CONTACT_TEMPLATE_ID`) — without these the contact form renders but won't
send. `NEXT_PUBLIC_RECAPTCHA_SITE_KEY`. `NOTION_API_KEY` (flight-data sync).
`NEXT_PUBLIC_SITE_URL` / `SITE_URL`. `NEXT_PUBLIC_CHAT_API_URL` (ChatBot widget only renders when
set). GoatCounter analytics: `GOATCOUNTER_API_KEY`, `GOATCOUNTER_SITE`,
`NEXT_PUBLIC_GOATCOUNTER_SITE`. `UPTIMEROBOT_API_KEY` (dashboard). `RESUME_SOURCE` (optional,
defaults to `~/personal/career/Mangonon_Dominic_Resume.html`).
130 changes: 1 addition & 129 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,129 +1 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Dom Mangonon's personal site: a **single-page** portfolio built with Next.js 16, React 19,
TypeScript, and Tailwind CSS 4. Static export (no server runtime), deployed to GitHub Pages
at https://dommango.github.io.

The page leads with the **project portfolio**, then writing, with career compressed to context.

## Commands

```bash
npm run dev # Start dev server (localhost:3000)
npm run build # Build static site (output: out/)
npm test # Run Vitest tests (watch mode)
npm test -- --run # Run tests once
npm run lint # ESLint check
npx tsc --noEmit # Type check
npx playwright test # E2E
```

Note: `npm install` needs `--legacy-peer-deps` (react-simple-maps declares a React <19 peer).
CI uses it too.

## Architecture

The whole site is **one page**. `app/page.tsx` is a server component that loads data at build
time and hands it to `components/landing/BrutalistLanding.tsx`, a client shell holding theme +
scroll-spy state and composing every section.

```
app/
├── layout.tsx # Root layout, fonts, metadata/JSON-LD, ChatBot (only if NEXT_PUBLIC_CHAT_API_URL is set)
├── page.tsx # Loads projects/writing/travel data -> BrutalistLanding
├── globals.css # All styles (see Design System below)
└── dashboard-m7x9k2/ # Private-ish analytics dashboard (obscure URL, not linked)

components/landing/ # The page, in DOM order:
├── Nav.tsx # Sticky nav + theme cycler
├── Availability.tsx # Status strip under nav
├── Hero.tsx # Headline, portrait, bio
├── Projects.tsx # THE MAIN SECTION — project cards
├── Writing.tsx # Substack posts; renders only when posts exist
├── Resume.tsx # Compressed career timeline
├── Travel.tsx # Continent bars + globe
├── Contact.tsx # EmailJS-wired form
├── Footer.tsx
└── BinaryRule.tsx # Decorative divider (seeded PRNG — see below)

components/travel/TravelMap.tsx # Heavy globe, dynamically imported by landing/Travel
components/chat/ChatBot.tsx # Assistant widget
components/ui/ # Card, Skeleton, ProgressBar — dashboard only

lib/content/ # Build-time data (no fs, no runtime fetch)
├── projects.ts # Hand-authored PROJECTS array
├── writing.ts # Substack posts
└── travel.ts # Transforms content/travel/*.json
lib/services/ # emailjs.ts, chat.ts
```

## Adding or changing a section

Four places must stay in sync or the scroll-spy breaks silently:

1. `components/landing/<Section>.tsx` — the component
2. `SectionId` union in `Nav.tsx`
3. `SECTION_IDS` in `BrutalistLanding.tsx` — **must match DOM order**; the spy takes the last
element with `offsetTop <= scrollY`, so wrong order = wrong active link
4. The `link()` calls in `Nav.tsx`

Conditional sections (Writing) need the nav link gated by the same predicate as the section,
or the link scrolls to nothing.

## Design System

All styles live in `app/globals.css` (~1130 lines). Two disjoint token systems:

- **`:root` (lines 1-121)** — legacy tokens + Tailwind bridge. Used by the dashboard/ChatBot only.
- **`.brutalist-root` (128-239)** — the landing. Everything is scoped here.

**Build new sections from semantic tokens only** — `var(--accent)`, `var(--fg)`, `var(--fg-muted)`,
`var(--fg-low)`, `var(--rule)`, `var(--s-N)` spacing, `var(--font-display|sans|mono)`. Do that and
all three themes (Gold / Oxblood / High Contrast) work for free, since the theme cycler only swaps
token values via `[data-accent]` / `[data-contrast]` attributes.

The landing is **dark-only by design** — the `prefers-color-scheme: light` block only touches
`:root`, which `.brutalist-root` shadows.

**Responsive: one breakpoint**, `@media (max-width: 900px)` at the bottom. Any new multi-column
grid must be added there manually; nothing is automatic.

Structure convention: `.section` > `<BinaryRule/>` > `.*-head`. The adjacent-sibling rule
`.section > .binary-rule + *` supplies the top margin, so keep that order.

`BinaryRule`'s `seed` prop drives a deterministic sine-hash PRNG — it exists for **hydration
safety** (server and client must render identical digits), not aesthetics. Give new sections an
arbitrary unused seed.

## Content

Hand-authored TypeScript in `lib/content/`, not markdown. Prefer typed TS modules over JSON:
`resolveJsonModule` is on, and importing an empty `[]` JSON file infers `never[]`, which fails
typecheck under `strict`.

Travel data is the exception — script-generated JSON in `content/travel/`, produced by
`scripts/process-travel-data.js` and `scripts/fetch-flights.js` (both manual).

**Career content is deliberately NOT in this repo** — it's gitignored. This repo is public; the
source of record is `~/personal/career`. Don't re-add it or reintroduce a sync step.

## Deployment

`.github/workflows/deploy.yml` — on push to `main` (+ manual), builds and pushes `out/` to Pages.

**Gotcha:** pushes made by `github-actions[bot]` with the default `GITHUB_TOKEN` do **not**
trigger workflows. So a cron that commits data cannot make the site rebuild. Anything needing
fresh data at deploy time must fetch **during the build**, not commit-then-rebuild.

`update-dashboard-data.yml` crons analytics/uptime/performance JSON into `public/data/`.

## Testing

Vitest + jsdom for units (`__tests__/`), Playwright for E2E (`e2e/`). `vitest.setup.ts` mocks
`matchMedia` and `IntersectionObserver`.

Test nontrivial logic (parsers, transforms, conditional-render invariants). Skip ceremony tests.
@AGENTS.md
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const metadata: Metadata = {
template: "%s | Dom Mangonon",
},
description:
"Dom Mangonon builds software with AI — SousIQ, Bracketeer, the Claude Code Placemat, and more. Projects, writing, and a travel map.",
"Dom Mangonon builds software with AI: SousIQ, Bracketeer, the Claude Code Placemat, and more. Projects, writing, and a travel map.",
openGraph: {
title: "Dom Mangonon",
description:
Expand All @@ -64,7 +64,7 @@ export const metadata: Metadata = {
url: "/og.png",
width: 1200,
height: 630,
alt: "Dom Mangonon — builds software with AI",
alt: "Dom Mangonon: builds software with AI",
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion components/landing/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function Hero() {
<div className="hero-meta-col">
<span className="ds-eyebrow">About</span>
<p className="hero-about">
Seventeen years in financial services — operations at BNP Paribas through 2008, an MBA
Seventeen years in financial services: operations at BNP Paribas through 2008, an MBA
at CMU Tepper, consulting at PwC, and now SVP at Citi.
</p>
<p className="hero-about hero-about-secondary">
Expand Down
2 changes: 1 addition & 1 deletion components/landing/Resume.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const TIMELINE = [
{
y: '2025 →',
t: 'Building with AI',
d: 'Shipping software nights and weekends with Claude Code — see above',
d: 'Shipping software nights and weekends with Claude Code (see above)',
},
{
y: '2021 →',
Expand Down
42 changes: 21 additions & 21 deletions lib/content/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,21 @@ export interface Project {

export const PROJECTS: Project[] = [
{
id: '#sous-0001/05',
id: '#plcm-0001/05',
name: 'Claude Code Placemat',
stack: 'Static HTML · GitHub Actions · scheduled agent',
year: '2026 →',
impact: 'Maintains itself · MIT',
points: [
'A one-page reference for Claude Code: shortcuts, slash commands, flags, hooks, MCP',
'A scheduled agent re-reads the latest release every day and opens a PR when anything drifts',
'Nobody updates it by hand',
],
href: 'https://dommango.github.io/claude-code-placemat/',
hrefKind: 'live',
},
{
id: '#sous-0002/05',
name: 'SousIQ',
stack: 'Express · React 19 · Postgres + pgvector · Claude',
year: '2025 →',
Expand All @@ -37,32 +51,18 @@ export const PROJECTS: Project[] = [
hrefKind: 'live',
},
{
id: '#brkt-0002/05',
id: '#brkt-0003/05',
name: 'Bracketeer',
stack: 'Next 16 · Prisma 7 · Auth.js · Railway',
year: '2026 →',
impact: 'Live · ran a real World Cup pool',
year: '2026',
impact: 'Ran a real World Cup pool · 40+ players',
points: [
'Create a pool, invite friends, make bracket picks, watch a leaderboard update from live results',
'Knockout seeding follows FIFA Annex C — the tiebreak rules are genuinely gnarly',
'Knockout seeding follows FIFA Annex C. The tiebreak rules are genuinely gnarly',
'Started as a pool for friends, grew into a multi-tenant platform',
],
href: 'https://fifawc26.up.railway.app',
hrefKind: 'live',
},
{
id: '#plcm-0003/05',
name: 'Claude Code Placemat',
stack: 'Static HTML · GitHub Actions · scheduled agent',
year: '2026 →',
impact: 'Maintains itself · MIT',
points: [
'A one-page reference for Claude Code: shortcuts, slash commands, flags, hooks, MCP',
'A scheduled agent re-reads the latest release every day and opens a PR when anything drifts',
'The interesting part is not the page — it is that nobody updates it by hand',
],
href: 'https://dommango.github.io/claude-code-placemat/',
hrefKind: 'live',
href: 'https://github.com/dommango/bracketeer',
hrefKind: 'repo',
},
{
id: '#modm-0004/05',
Expand Down
Loading