|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guidance for Claude Code when working in this repository. Read this first — it |
| 4 | +is meant to save you from re-exploring the codebase on every session. |
| 5 | + |
| 6 | +## What this is |
| 7 | + |
| 8 | +Personal portfolio + playground website for **bbastian.dev** (owner: Bastian, |
| 9 | +GitHub `bastian-js`, developer/student, Austria). A React SPA frontend plus a |
| 10 | +small Express API. The site hosts the portfolio, some mini-apps (PiggyTrack, |
| 11 | +DropNote, ProPerform), landing pages for side projects (Noury), Spotify stats |
| 12 | +pages, hidden easter-egg games, and interactive features like **Leave a Word**. |
| 13 | + |
| 14 | +- **Live frontend:** https://bbastian.dev |
| 15 | +- **Live API:** https://api.bbastian.dev (referenced in code as `const API = "https://api.bbastian.dev"`) |
| 16 | +- Design tone: dark, minimal, "developer aesthetic", emerald accent. |
| 17 | + |
| 18 | +## Tech stack |
| 19 | + |
| 20 | +**Frontend** (repo root) |
| 21 | +- React 19 + TypeScript, Vite 7, React Router v7 (`react-router-dom`) |
| 22 | +- Tailwind CSS v4 (via `@tailwindcss/vite`) — utility classes **plus** heavy use of inline `style={{}}` objects for colors/borders/animations |
| 23 | +- `lucide-react` for icons, `chart.js` + `react-chartjs-2` for charts, `canvas-confetti`, `react-snowfall` |
| 24 | +- Dev server runs on Vite (port 5173). Preview build on 3030. |
| 25 | + |
| 26 | +**Backend** (`server/`) |
| 27 | +- Node ESM (`"type": "module"`), Express 4, `mysql2/promise` connection pool |
| 28 | +- `zod` for input validation, `express-rate-limit` for rate limiting |
| 29 | +- `resend` + `nodemailer` for the contact form, Spotify Web API integration |
| 30 | +- Runs on `process.env.PORT`. DB config via `DB_HOST/DB_USER/DB_PASS/DB_NAME/DB_PORT` (see `server/.env.example`). |
| 31 | + |
| 32 | +## Commands |
| 33 | + |
| 34 | +Frontend (from repo root): |
| 35 | +- `npm run dev` — Vite dev server |
| 36 | +- `npm run build` — `tsc -b && vite build` (typecheck THEN build) |
| 37 | +- `npm run lint` — ESLint |
| 38 | +- `npx tsc -b` — typecheck only (fast; use this to verify TS changes) |
| 39 | + |
| 40 | +Backend (from `server/`): |
| 41 | +- `node index.js` — start the API (needs `server/.env`) |
| 42 | +- No test suite exists in either package. |
| 43 | + |
| 44 | +There are **no automated tests**. Verify frontend changes with `npx tsc -b` |
| 45 | +(and `npm run lint` for anything non-trivial). Verify server changes by reading |
| 46 | +the affected route carefully — there is no test harness. |
| 47 | + |
| 48 | +## Project layout |
| 49 | + |
| 50 | +``` |
| 51 | +/ Frontend (Vite root) |
| 52 | + index.html |
| 53 | + src/ |
| 54 | + App.tsx Router shell + global keyboard shortcuts + easter eggs |
| 55 | + routes.tsx Central route table — add new pages here |
| 56 | + main.tsx Entry |
| 57 | + globals.css / index.css |
| 58 | + components/ Shared UI (NavBar, Footer, FadeIn, games, overlays, AnnouncementBanner, SplashScreen…) |
| 59 | + pages/ One file per route (Home, Projects, About, Contact, Now, LeaveAWord, PiggyTrack…) |
| 60 | + noury/ Noury side-project landing + legal pages |
| 61 | + spotify/ Spotify stats / hall-of-fame / OAuth callback pages |
| 62 | + public/ |
| 63 | + dist/ Build output (committed) |
| 64 | +server/ |
| 65 | + index.js ALL API routes live here (one big file, ~1050 lines) |
| 66 | + db.js mysql2 pool export (`db`) |
| 67 | + bad-words.json Banned-words list ({ "words": [...] }) for content filtering |
| 68 | + get-spotify-token.mjs One-off script to mint a Spotify refresh token |
| 69 | + .env / .env.example |
| 70 | +``` |
| 71 | + |
| 72 | +## Conventions & patterns (match these) |
| 73 | + |
| 74 | +- **Styling:** Tailwind utilities for layout; inline `style={{}}` for colors, |
| 75 | + borders, and hover transitions. Hover effects are done with |
| 76 | + `onMouseEnter`/`onMouseLeave` mutating `e.currentTarget.style`, not CSS |
| 77 | + `:hover`. Follow the existing component you're editing. |
| 78 | +- **Color palette:** background `#0a0a0a` / cards `#111`; emerald accent |
| 79 | + `#34d399` (tints used a lot: `#34d39918`, `#34d39930`); text white with |
| 80 | + `rgba(255,255,255,α)` for muted greys; hairline borders |
| 81 | + `1px solid rgba(255,255,255,0.05–0.09)`; cards are `rounded-xl`. |
| 82 | +- **Fonts:** Inter for UI, monospace (`Courier New`) for accents/labels/counters. |
| 83 | +- **Animations:** `FadeIn` component wraps content with a `delay` prop |
| 84 | + (`delay={index * 40}`) for staggered entrance. Keyframes are injected via |
| 85 | + inline `<style>` tags inside components (see Footer, SplashScreen). |
| 86 | +- **Adding a page:** create `src/pages/Foo.tsx`, import + register it in |
| 87 | + `src/routes.tsx`. If it should appear in nav, add to `NAV_LINKS` in |
| 88 | + `src/components/Footer.tsx` (and NavBar if relevant). |
| 89 | +- **Keyboard shortcuts & easter eggs** live in `App.tsx`: single-key nav |
| 90 | + (h/p/s/a/c/n/l/g), Ctrl/Cmd+K & `/` search, `?` shortcuts overlay, Konami |
| 91 | + code → hacker terminal, typed `mine`/`snake`/`matrix` → games. |
| 92 | +- **localStorage/sessionStorage:** always wrap access in a try/catch helper |
| 93 | + (private-mode / quota safety). See `safeLocalStorage` in `LeaveAWord.tsx` and |
| 94 | + `AnnouncementBanner.tsx`, and `SESSION_KEY` guard in `SplashScreen.tsx`. |
| 95 | +- **Windows environment:** default shell is PowerShell; a Bash tool is also |
| 96 | + available. Prefer forward-slash paths. |
| 97 | + |
| 98 | +## Server / API notes |
| 99 | + |
| 100 | +Base URL `https://api.bbastian.dev`. All routes are defined in `server/index.js`. |
| 101 | +CORS allows `bbastian.dev` + any `*.bbastian.dev` subdomain + localhost. Key |
| 102 | +endpoints: |
| 103 | + |
| 104 | +- `GET /github-stats` — cached GitHub profile/repo/language stats |
| 105 | +- Spotify: `GET /spotify/{auth,exchange,now-playing,top-tracks,hall-of-fame,artist-hall-of-fame}` |
| 106 | +- `POST /contact` (rate-limited) — sends email via Resend/nodemailer, zod-validated |
| 107 | +- `POST /noury/waitlist`, `GET /noury/waitlist/count` |
| 108 | +- Leaderboards: `GET|POST /leaderboard/{minesweeper,snake}` (POST rate-limited) |
| 109 | +- **Leave a Word:** `POST /leave-a-word` (rate-limited, cookie `visitor_id`), |
| 110 | + `GET /leave-a-word/all?page=&sort=newest|oldest` |
| 111 | +- Visitors: `POST /visitors/ping`, `GET /visitors/count` |
| 112 | +- **Live cursors (WebSocket):** `wss://api.bbastian.dev/cursors`. Client sends |
| 113 | + `{x,y}` (normalized 0–1); server assigns a random color per connection |
| 114 | + and broadcasts `{type:"state",cursors:[{id,x,y,color}]}` at ~20fps |
| 115 | + (each client gets everyone *except* itself) plus `{type:"leave",id}`. The |
| 116 | + Express `app` is wrapped in an `http.createServer` so `ws` can share the port |
| 117 | + (`server.listen`, upgrade handler filters path `/cursors` + origin). |
| 118 | + |
| 119 | +**Content filtering:** `normalizeWord()` folds leetspeak, German umlauts, |
| 120 | +spacing and accents down to `[a-z0-9]` before comparing against |
| 121 | +`bad-words.json`. `isBannedWord()` uses it for both submitted words and names. |
| 122 | + |
| 123 | +**Leave a Word validation rules** (`validateSubmittedWord` + `leaveAWordNameSchema`): |
| 124 | +- Word: 2–51 chars, **exactly one word** (no whitespace), not banned. |
| 125 | +- Name: 2–30 chars, no leading/trailing space, no line breaks/tabs, **≤ 3 |
| 126 | + words** (a name, not a sentence), not banned. |
| 127 | +- The frontend (`src/pages/LeaveAWord.tsx`) mirrors these: name input |
| 128 | + `maxLength=30`, word input strips whitespace on change (`maxLength=51`). |
| 129 | + |
| 130 | +## Features added / gotchas |
| 131 | + |
| 132 | +- **AnnouncementBanner** (`src/components/AnnouncementBanner.tsx`, rendered in |
| 133 | + `App.tsx`): bottom-**right** "cookie-style" nudge for Leave a Word. Stores no |
| 134 | + cookie — dismissal is persisted in `localStorage["law_banner_dismissed"]="1"` |
| 135 | + (permanent). Hidden on the `/leave-a-word` page itself. Slide-in/out via |
| 136 | + transform+opacity. |
| 137 | +- **SplashScreen** (`src/components/SplashScreen.tsx`): split-panel intro shown |
| 138 | + once per session (`sessionStorage["bbastian_splash_shown"]`). A module-level |
| 139 | + `_started` guard prevents React StrictMode double-init. Timers intentionally |
| 140 | + have no cleanup so they survive StrictMode remount. Total run ≈ 1.65s. |
| 141 | +- **LiveCursors** (`src/components/LiveCursors.tsx`, mounted in `App.tsx`): |
| 142 | + Figma-style multiplayer cursors on a click-through `<canvas>`. Two config |
| 143 | + consts at the top of the file: `TEST` (bool) and `TEST_CURSOR_COUNT` (0–1000). |
| 144 | + `TEST=true` simulates wandering fake cursors (no backend). `TEST=false` |
| 145 | + connects to the `/cursors` WebSocket — colors come from the server, the |
| 146 | + client only broadcasts its own `{x,y}`. Colored arrow only, no name labels. |
| 147 | + Canvas-rendered so it scales to 1000. |
| 148 | +- `dist/` is committed — rebuild it if the frontend needs to be deployed from |
| 149 | + the repo. |
| 150 | + |
| 151 | +## Working agreements |
| 152 | + |
| 153 | +- Commit or push only when explicitly asked. Default branch is `main`. |
| 154 | +- The owner communicates in German; code, comments, and identifiers stay in |
| 155 | + English (existing inline comments are a mix of German/English — match the file |
| 156 | + you're editing). |
| 157 | +- Keep this file up to date when you add features, routes, or conventions so |
| 158 | + future sessions stay cheap. |
0 commit comments