diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ee6c38e..312206c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,210 +1,107 @@ # Portfolio Project Instructions -This is Dennis Konkol's personal portfolio (dk0.dev) - a Next.js 15 portfolio with Directus CMS integration, n8n automation, and a "liquid" design system. +Dennis Konkol's portfolio (dk0.dev) — Next.js 15, Directus CMS, n8n automation, "Liquid Editorial Bento" design system. ## Build, Test, and Lint -### Development ```bash -npm run dev # Full dev environment (Docker + Next.js) -npm run dev:simple # Next.js only (no Docker dependencies) -npm run dev:next # Plain Next.js dev server +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 ``` -### Build & Deploy -```bash -npm run build # Production build (standalone mode) -npm run start # Start production server -``` +## Architecture -### Testing -```bash -# Unit tests (Jest) -npm run test # Run all unit tests -npm run test:watch # Watch mode -npm run test:coverage # With coverage report +### Server/Client Component Split -# E2E tests (Playwright) -npm run test:e2e # Run all E2E tests -npm run test:e2e:ui # Interactive UI mode -npm run test:critical # Critical paths only -npm run test:hydration # Hydration tests only -``` +The homepage uses a **server component orchestrator** pattern: -### Linting -```bash -npm run lint # Run ESLint -npm run lint:fix # Auto-fix issues -``` +- `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 -### Database (Prisma) -```bash -npm run db:generate # Generate Prisma client -npm run db:push # Push schema to database -npm run db:studio # Open Prisma Studio -npm run db:seed # Seed database -``` +### SSR Animation Safety -## Architecture Overview +**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. -### Tech Stack -- **Framework**: Next.js 15 (App Router), TypeScript 5.9 -- **Styling**: Tailwind CSS 3.4 with custom `liquid-*` color tokens -- **Theming**: next-themes for dark mode (system/light/dark) -- **Animations**: Framer Motion 12 -- **3D**: Three.js + React Three Fiber (shader gradient background) -- **Database**: PostgreSQL via Prisma ORM -- **Cache**: Redis (optional) -- **CMS**: Directus (self-hosted, GraphQL, optional) -- **Automation**: n8n webhooks (status, chat, hardcover, image generation) -- **i18n**: next-intl (EN + DE) -- **Monitoring**: Console error logging (development mode only) -- **Deployment**: Docker (standalone mode) + Nginx +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. -### Key Directories -``` -app/ - [locale]/ # i18n routes (en, de) - page.tsx # Homepage sections - projects/ # Project listing + detail pages - api/ # API routes - book-reviews/ # Book reviews from Directus - hobbies/ # Hobbies from Directus - n8n/ # n8n webhook proxies - projects/ # Projects (PostgreSQL + Directus) - tech-stack/ # Tech stack from Directus - components/ # React components -lib/ - directus.ts # Directus GraphQL client (no SDK) - auth.ts # Auth + rate limiting - translations-loader.ts # i18n loaders for server components -prisma/ - schema.prisma # Database schema -messages/ - en.json # English translations - de.json # German translations -``` +Framer Motion `AnimatePresence` is fine for modals/overlays that only render after user interaction. ### Data Source Fallback Chain -The architecture prioritizes resilience with this fallback hierarchy: -1. **Directus CMS** (if `DIRECTUS_STATIC_TOKEN` configured) -2. **PostgreSQL** (for projects, analytics) -3. **JSON files** (`messages/*.json`) -4. **Hardcoded defaults** -5. **Display key itself** (last resort) -**Critical**: The site never crashes if external services (Directus, PostgreSQL, n8n, Redis) are unavailable. All API routes return graceful fallbacks. +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 calls via `lib/directus.ts` (no Directus SDK) -- Collections: `tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews` -- Translations use Directus native system (M2O to `languages`) + +- 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 export `runtime='nodejs'`, `dynamic='force-dynamic'` and include a `source` field in JSON responses (`directus|fallback|error`) +- API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and return `source` field (`directus|fallback|error`) ### n8n Integration -- Webhook base URL: `N8N_WEBHOOK_URL` env var -- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers -- All endpoints have rate limiting and 10s timeout protection -- Hardcover reading data cached for 5 minutes + +- 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 (Internationalization) -- **Supported locales**: `en` (English), `de` (German) -- **Primary source**: Static JSON files in `messages/en.json` and `messages/de.json` -- **Optional override**: Directus CMS `messages` collection -- **Server components**: Use `getHeroTranslations()`, `getNavTranslations()`, etc. from `lib/translations-loader.ts` -- **Client components**: Use `useTranslations("key.path")` from next-intl -- **Locale mapping**: Middleware defines `["en", "de"]` which must match `app/[locale]/layout.tsx` +### i18n -### Component Patterns -- **Client components**: Mark with `"use client"` for interactive/data-fetching parts -- **Data loading**: Use `useEffect` for client-side fetching on mount -- **Animations**: Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp` -- **Loading states**: Every async component needs a matching Skeleton component +- 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 ("Liquid Editorial Bento") -- **Core palette**: Cream (`#fdfcf8`), Stone (`#0c0a09`), Emerald (`#10b981`) -- **Custom colors**: Prefixed with `liquid-*` (sky, mint, lavender, pink, rose, peach, coral, teal, lime) -- **Card style**: Gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`) -- **Glassmorphism**: Use `backdrop-blur-sm` with `border-2` and `rounded-xl` -- **Typography**: Headlines uppercase, tracking-tighter, with accent point at end -- **Layout**: Bento Grid for new features (no floating overlays) +### Design System -### File Naming -- **Components**: PascalCase in `app/components/` (e.g., `About.tsx`) -- **API routes**: kebab-case directories in `app/api/` (e.g., `book-reviews/`) -- **Lib utilities**: kebab-case in `lib/` (e.g., `email-obfuscate.ts`) +- 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 (`.`) +- 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 -- **Language**: Code in English, user-facing text via i18n -- **TypeScript**: No `any` types - use interfaces from `lib/directus.ts` or `app/_ui/` -- **Error handling**: All API calls must catch errors with fallbacks -- **Error logging**: Only in development mode (`process.env.NODE_ENV === "development"`) -- **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`) -- **No emojis**: Unless explicitly requested -### Testing Notes -- **Jest environment**: JSDOM with mocks for `window.matchMedia` and `IntersectionObserver` -- **Playwright**: Uses plain Next.js dev server (no Docker) with `NODE_ENV=development` to avoid Edge runtime issues -- **Transform**: ESM modules (react-markdown, remark-*, etc.) are transformed via `transformIgnorePatterns` -- **After UI changes**: Run `npm run test` to verify no regressions +- 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 `` tag ### Docker & Deployment -- **Standalone mode**: `next.config.ts` uses `output: "standalone"` for optimized Docker builds -- **Branches**: `dev` → staging, `production` → live -- **CI/CD**: Gitea Actions (`.gitea/workflows/`) -- **Verify Docker builds**: Always test Docker builds after changes to `next.config.ts` or dependencies + +- `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//route.ts` with `runtime='nodejs'` and `dynamic='force-dynamic'` -3. Create component in `app/components/.tsx` -4. Add i18n keys to `messages/en.json` and `messages/de.json` -5. Integrate into parent component - -### Adding i18n strings -1. Add keys to both `messages/en.json` and `messages/de.json` -2. Use `useTranslations("key.path")` in client components -3. Use `getTranslations("key.path")` in server components - -### Working with Directus -- All queries go through `directusRequest()` in `lib/directus.ts` -- Uses GraphQL endpoint (`/graphql`) with 2s timeout -- Returns `null` on failure (graceful degradation) -- Translations filtered by `languages_code.code` matching Directus locale - -## Environment Variables - -### Required for CMS -```bash -DIRECTUS_URL=https://cms.dk0.dev -DIRECTUS_STATIC_TOKEN=... -``` - -### Required for n8n features -```bash -N8N_WEBHOOK_URL=https://n8n.dk0.dev -N8N_SECRET_TOKEN=... -N8N_API_KEY=... -``` - -### Database & Cache -```bash -DATABASE_URL=postgresql://... -REDIS_URL=redis://... -``` - -### Optional -```bash -NEXT_PUBLIC_BASE_URL=https://dk0.dev -``` - -## Documentation References -- Operations guide: `docs/OPERATIONS.md` -- Locale system: `docs/LOCALE_SYSTEM.md` -- CMS guide: `docs/CMS_GUIDE.md` -- Testing & deployment: `docs/TESTING_AND_DEPLOYMENT.md` +3. Create component in `app/components/.tsx` with Skeleton loading state +4. Add i18n keys to both `messages/en.json` and `messages/de.json` +5. Create a `Client` wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider` +6. Add to `HomePageServer.tsx` wrapped in `ScrollFadeIn`