- Document ScrollFadeIn pattern and Framer Motion SSR pitfall - Update server/client component architecture section - Reflect combined CI/CD workflow structure - Add accessibility contrast requirements - Streamline commands and conventions sections Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
108 lines
5.5 KiB
Markdown
108 lines
5.5 KiB
Markdown
# Portfolio Project Instructions
|
|
|
|
Dennis Konkol's portfolio (dk0.dev) — Next.js 15, Directus CMS, n8n automation, "Liquid Editorial Bento" design system.
|
|
|
|
## Build, Test, and Lint
|
|
|
|
```bash
|
|
npm run dev:next # Plain Next.js dev server (no Docker)
|
|
npm run build # Production build (standalone mode)
|
|
npm run lint # ESLint (0 errors required, warnings OK)
|
|
npm run lint:fix # Auto-fix lint issues
|
|
npm run test # All Jest unit tests
|
|
npx jest path/to/test.tsx # Run a single test file
|
|
npm run test:watch # Watch mode
|
|
npm run test:e2e # Playwright E2E tests
|
|
npm run db:generate # Regenerate Prisma client after schema changes
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Server/Client Component Split
|
|
|
|
The homepage uses a **server component orchestrator** pattern:
|
|
|
|
- `app/_ui/HomePageServer.tsx` — async server component, fetches all translations in parallel via `Promise.all`, renders Hero directly, wraps client sections in `ScrollFadeIn`
|
|
- `app/components/Hero.tsx` — **server component** (no `"use client"`), uses `getTranslations()` from `next-intl/server`
|
|
- `app/components/ClientWrappers.tsx` — exports `AboutClient`, `ProjectsClient`, `ContactClient`, `FooterClient`, each wrapping their component in a scoped `NextIntlClientProvider` with only the needed translation keys
|
|
- `app/components/ClientProviders.tsx` — root client wrapper, defers Three.js/WebGL via `requestIdleCallback` (5s timeout) to avoid blocking LCP
|
|
|
|
### SSR Animation Safety
|
|
|
|
**Never use Framer Motion's `initial={{ opacity: 0 }}` on SSR-rendered elements** — it bakes `style="opacity:0"` into HTML, making content invisible if hydration fails.
|
|
|
|
Use `ScrollFadeIn` component instead (`app/components/ScrollFadeIn.tsx`): renders no inline style during SSR (content visible by default), applies opacity+transform only after `hasMounted` check, animates via IntersectionObserver + CSS transitions.
|
|
|
|
Framer Motion `AnimatePresence` is fine for modals/overlays that only render after user interaction.
|
|
|
|
### Data Source Fallback Chain
|
|
|
|
Every data fetch degrades gracefully — the site never crashes:
|
|
|
|
1. **Directus CMS** → 2. **PostgreSQL** → 3. **JSON files** (`messages/*.json`) → 4. **Hardcoded defaults** → 5. **i18n key itself**
|
|
|
|
### CMS Integration (Directus)
|
|
|
|
- GraphQL via `lib/directus.ts` — no Directus SDK, uses `directusRequest()` with 2s timeout
|
|
- Returns `null` on failure (never throws)
|
|
- Locale mapping: `en` → `en-US`, `de` → `de-DE`
|
|
- API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and return `source` field (`directus|fallback|error`)
|
|
|
|
### n8n Integration
|
|
|
|
- Webhook proxies in `app/api/n8n/` (status, chat, hardcover, generate-image)
|
|
- Auth: `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
|
|
- All endpoints have rate limiting and 10s timeout
|
|
- Hardcover reading data cached 5 minutes
|
|
|
|
## Key Conventions
|
|
|
|
### i18n
|
|
|
|
- Locales: `en`, `de` — defined in `middleware.ts`, must match `app/[locale]/layout.tsx`
|
|
- Client components: `useTranslations("key.path")` from `next-intl`
|
|
- Server components: `getTranslations("key.path")` from `next-intl/server`
|
|
- Always add keys to both `messages/en.json` and `messages/de.json`
|
|
|
|
### Design System
|
|
|
|
- Custom Tailwind colors: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime`
|
|
- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm`, `border-2`, `rounded-xl`
|
|
- Typography: Headlines uppercase, `tracking-tighter`, accent dot at end (`<span className="text-emerald-600">.</span>`)
|
|
- Layout: Bento Grid, no floating overlays
|
|
- Accessibility: Use `text-stone-600 dark:text-stone-400` (not `text-stone-400`) for body text — contrast ratio must be ≥4.5:1
|
|
|
|
### Code Style
|
|
|
|
- TypeScript: no `any` — use interfaces from `lib/directus.ts` or `types/`
|
|
- Error logging: `console.error` only when `process.env.NODE_ENV === "development"`
|
|
- File naming: PascalCase components (`About.tsx`), kebab-case API routes (`book-reviews/`), kebab-case lib utils
|
|
- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`)
|
|
- Every async component needs a Skeleton loading state
|
|
|
|
### Testing
|
|
|
|
- Jest with JSDOM; mocks for `window.matchMedia` and `IntersectionObserver` in `jest.setup.ts`
|
|
- ESM modules transformed via `transformIgnorePatterns` (react-markdown, remark-*, etc.)
|
|
- Server component tests: `const resolved = await Component({ props }); render(resolved)`
|
|
- Test mocks for `next/image`: use `eslint-disable-next-line @next/next/no-img-element` on the `<img>` tag
|
|
|
|
### Docker & Deployment
|
|
|
|
- `output: "standalone"` in `next.config.ts`
|
|
- Entrypoint: `scripts/start-with-migrate.js` — waits for DB, runs migrations (non-fatal on failure), starts server
|
|
- CI/CD: `.gitea/workflows/ci.yml` — `test-build` job (all branches), `deploy-dev` (dev only), `deploy-production` (production only)
|
|
- Branches: `dev` → testing.dk0.dev, `production` → dk0.dev
|
|
- Dev and production share the same PostgreSQL and Redis instances
|
|
|
|
## Common Tasks
|
|
|
|
### Adding a CMS-managed section
|
|
|
|
1. Define GraphQL query + types in `lib/directus.ts`
|
|
2. Create API route in `app/api/<name>/route.ts` with `runtime='nodejs'` and `dynamic='force-dynamic'`
|
|
3. Create component in `app/components/<Name>.tsx` with Skeleton loading state
|
|
4. Add i18n keys to both `messages/en.json` and `messages/de.json`
|
|
5. Create a `<Name>Client` wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider`
|
|
6. Add to `HomePageServer.tsx` wrapped in `ScrollFadeIn`
|