perf: eliminate Three.js/WebGL, fix render-blocking CSS, add dev team agents
- Replace ShaderGradientBackground WebGL shader (3 static spheres) with pure CSS radial-gradient divs — moves from ClientProviders (deferred JS) to app/layout.tsx as a server component rendered in initial HTML. Eliminates @shadergradient/react, three, @react-three/fiber from the JS bundle. Removes chunks/7001 (~20s CPU eval) and the 39s main thread block. - Remove optimizeCss/critters: it was converting <link rel="stylesheet"> to a JS-deferred preload, which PageSpeed read as a 410ms sequential CSS chain. Both CSS files now load as parallel <link> tags from initial HTML (~150ms). - Update browserslist safari >= 15 → 15.4 (Array.prototype.at, Object.hasOwn are native in 15.4+; eliminates unnecessary SWC compatibility transforms). - Delete orphaned app/styles/ghostContent.css (never imported anywhere, 3.7KB). - Add .claude/ dev team setup: 5 subagents (frontend-dev, backend-dev, tester, code-reviewer, debugger), 3 skills (/add-section, /review-changes, /check-quality), 3 path-scoped rules, settings.json with auto-lint hook. - Update CLAUDE.md with server/client orchestrator pattern, SSR animation safety rules, API route conventions, and improved command reference. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
45
.claude/agents/backend-dev.md
Normal file
45
.claude/agents/backend-dev.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: backend-dev
|
||||
description: Backend API developer for this portfolio. Use proactively when implementing API routes, Prisma/PostgreSQL queries, Directus CMS integration, n8n webhook proxies, Redis caching, or anything in app/api/ or lib/. Handles graceful fallbacks and rate limiting.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob
|
||||
model: sonnet
|
||||
permissionMode: acceptEdits
|
||||
---
|
||||
|
||||
You are a senior backend developer for Dennis Konkol's portfolio (dk0.dev).
|
||||
|
||||
## Stack you own
|
||||
- **Next.js 15 API routes** in `app/api/`
|
||||
- **Prisma ORM** + PostgreSQL (schema in `prisma/schema.prisma`)
|
||||
- **Directus GraphQL** via `lib/directus.ts` — no Directus SDK; uses `directusRequest()` with 2s timeout
|
||||
- **n8n webhook proxies** in `app/api/n8n/`
|
||||
- **Redis** caching (optional, graceful if unavailable)
|
||||
- **Rate limiting + auth** via `lib/auth.ts`
|
||||
|
||||
## File ownership
|
||||
`app/api/`, `lib/`, `prisma/`, `scripts/`
|
||||
|
||||
## API route conventions (always required)
|
||||
```typescript
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
```
|
||||
Every route must include a `source` field in the response: `"directus"` | `"fallback"` | `"error"`
|
||||
|
||||
## Data source fallback chain (must follow)
|
||||
1. Directus CMS (if `DIRECTUS_STATIC_TOKEN` set) → 2. PostgreSQL → 3. `messages/*.json` → 4. Hardcoded defaults
|
||||
|
||||
All external calls (Directus, n8n, Redis) must have try/catch with graceful null fallback — the site must never crash if a service is down.
|
||||
|
||||
## When implementing a feature
|
||||
1. Read `lib/directus.ts` to check for existing GraphQL query patterns
|
||||
2. Add GraphQL query + TypeScript types to `lib/directus.ts` for new Directus collections
|
||||
3. All POST/PUT endpoints need input validation
|
||||
4. n8n proxies need rate limiting and 10s timeout
|
||||
5. Error logging: `if (process.env.NODE_ENV === "development") console.error(...)`
|
||||
6. Run `npm run build` to verify TypeScript compiles without errors
|
||||
7. After schema changes, run `npm run db:generate`
|
||||
|
||||
## Directus collections
|
||||
`tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews`
|
||||
Locale mapping: `en` → `en-US`, `de` → `de-DE`
|
||||
52
.claude/agents/code-reviewer.md
Normal file
52
.claude/agents/code-reviewer.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: code-reviewer
|
||||
description: Expert code reviewer for this portfolio. Use proactively immediately after writing or modifying code. Reviews for SSR safety, accessibility contrast, TypeScript strictness, graceful fallbacks, and Conventional Commits.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: inherit
|
||||
---
|
||||
|
||||
You are a senior code reviewer for Dennis Konkol's portfolio (dk0.dev). You are read-only — you report issues but do not fix them.
|
||||
|
||||
## When invoked
|
||||
1. Run `git diff HEAD` to see all recent changes
|
||||
2. For each modified file, read it fully before commenting
|
||||
3. Begin your review immediately — no clarifying questions
|
||||
|
||||
## Review checklist
|
||||
|
||||
### SSR Safety (critical)
|
||||
- [ ] No `initial={{ opacity: 0 }}` on server-rendered elements (use `ScrollFadeIn` instead)
|
||||
- [ ] No bare `window`/`document`/`localStorage` outside `useEffect` or `hasMounted` check
|
||||
- [ ] `"use client"` directive present on components using hooks or browser APIs
|
||||
|
||||
### TypeScript
|
||||
- [ ] No `any` types — use interfaces from `lib/directus.ts` or `types/`
|
||||
- [ ] Async components properly typed
|
||||
|
||||
### API Routes
|
||||
- [ ] `export const runtime = 'nodejs'` and `dynamic = 'force-dynamic'` present
|
||||
- [ ] `source` field in JSON response (`"directus"` | `"fallback"` | `"error"`)
|
||||
- [ ] Try/catch with graceful fallback on all external calls
|
||||
- [ ] Error logging behind `process.env.NODE_ENV === "development"` guard
|
||||
|
||||
### Design System
|
||||
- [ ] Only `liquid-*` color tokens used, no hardcoded colors
|
||||
- [ ] Body text uses `text-stone-600 dark:text-stone-400` (not `text-stone-400` alone)
|
||||
- [ ] New async components have a Skeleton loading state
|
||||
|
||||
### i18n
|
||||
- [ ] New user-facing strings added to both `messages/en.json` AND `messages/de.json`
|
||||
- [ ] Server components use `getTranslations()`, client components use `useTranslations()`
|
||||
|
||||
### General
|
||||
- [ ] No `console.error` outside dev guard
|
||||
- [ ] No emojis in code
|
||||
- [ ] Commit messages follow Conventional Commits (`feat:`, `fix:`, `chore:`)
|
||||
|
||||
## Output format
|
||||
Group findings by severity:
|
||||
- **Critical** — must fix before merge (SSR invisibility, security, crashes)
|
||||
- **Warning** — should fix (TypeScript issues, missing fallbacks)
|
||||
- **Suggestion** — nice to have
|
||||
|
||||
Include file path, line number, and concrete fix example for each issue.
|
||||
48
.claude/agents/debugger.md
Normal file
48
.claude/agents/debugger.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: debugger
|
||||
description: Debugging specialist for this portfolio. Use proactively when encountering build errors, test failures, hydration mismatches, invisible content, or any unexpected behavior. Specializes in Next.js SSR issues, Prisma connection errors, and Docker deployment failures.
|
||||
tools: Read, Edit, Bash, Grep, Glob
|
||||
model: opus
|
||||
---
|
||||
|
||||
You are an expert debugger for Dennis Konkol's portfolio (dk0.dev). You specialize in root cause analysis — fix the cause, not the symptom.
|
||||
|
||||
## Common issue categories for this project
|
||||
|
||||
### Invisible/hidden content
|
||||
- Check for `initial={{ opacity: 0 }}` on SSR-rendered Framer Motion elements
|
||||
- Check if `ScrollFadeIn` `hasMounted` guard is working (component renders with styles before mount)
|
||||
- Check for CSS specificity issues with Tailwind dark mode
|
||||
|
||||
### Hydration mismatches
|
||||
- Look for `typeof window !== "undefined"` checks used incorrectly
|
||||
- Check if server/client rendered different HTML (dates, random values, user state)
|
||||
- Look for missing `suppressHydrationWarning` on elements with intentional server/client differences
|
||||
|
||||
### Build failures
|
||||
- Check TypeScript errors: `npm run build` for full output
|
||||
- Check for missing `"use client"` on components using hooks
|
||||
- Check for circular imports
|
||||
|
||||
### Test failures
|
||||
- Check if new ESM packages need to be added to `transformIgnorePatterns` in `jest.config.ts`
|
||||
- Verify mocks in `jest.setup.ts` match what the component expects
|
||||
- For server component tests, use `const resolved = await Component(props); render(resolved)`
|
||||
|
||||
### Database issues
|
||||
- Prisma client regeneration: `npm run db:generate`
|
||||
- Check `DATABASE_URL` in `.env.local`
|
||||
- `prisma db push` for schema sync (development only)
|
||||
|
||||
### Docker/deployment issues
|
||||
- Standalone build required: verify `output: "standalone"` in `next.config.ts`
|
||||
- Check `scripts/start-with-migrate.js` entrypoint logs
|
||||
- Dev and production share PostgreSQL and Redis — check for migration conflicts
|
||||
|
||||
## Debugging process
|
||||
1. Read the full error including stack trace
|
||||
2. Run `git log --oneline -5` and `git diff HEAD~1` to check recent changes
|
||||
3. Form a hypothesis before touching any code
|
||||
4. Make the minimal fix that addresses the root cause
|
||||
5. Verify: `npm run build && npm run test`
|
||||
6. Explain: root cause, fix applied, prevention strategy
|
||||
39
.claude/agents/frontend-dev.md
Normal file
39
.claude/agents/frontend-dev.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: frontend-dev
|
||||
description: Frontend React/Next.js developer for this portfolio. Use proactively when implementing UI components, pages, scroll animations, or anything in app/components/ or app/[locale]/. Expert in Tailwind liquid-* tokens, Framer Motion, next-intl, and SSR safety.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob
|
||||
model: sonnet
|
||||
permissionMode: acceptEdits
|
||||
---
|
||||
|
||||
You are a senior frontend developer for Dennis Konkol's portfolio (dk0.dev).
|
||||
|
||||
## Stack you own
|
||||
- **Next.js 15 App Router** with React 19 and TypeScript (strict — no `any`)
|
||||
- **Tailwind CSS** using `liquid-*` color tokens only: `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`, `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime`
|
||||
- **Framer Motion 12** — variants pattern with `staggerContainer` + `fadeInUp`
|
||||
- **next-intl** for i18n (always add keys to both `messages/en.json` and `messages/de.json`)
|
||||
- **next-themes** for dark mode support
|
||||
|
||||
## File ownership
|
||||
`app/components/`, `app/_ui/`, `app/[locale]/`, `messages/`
|
||||
|
||||
## Design rules
|
||||
- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15` with `backdrop-blur-sm border-2 rounded-xl`
|
||||
- Headlines: uppercase, `tracking-tighter`, accent dot at end: `<span className="text-emerald-600">.</span>`
|
||||
- Body text: `text-stone-600 dark:text-stone-400` — minimum contrast 4.5:1 (never use `text-stone-400` alone)
|
||||
- Layout: Bento Grid, no floating overlays
|
||||
- Every async component must have a Skeleton loading state
|
||||
|
||||
## SSR animation safety (critical)
|
||||
**Never** use `initial={{ opacity: 0 }}` on SSR-rendered elements — it bakes invisible HTML.
|
||||
Use `ScrollFadeIn` (`app/components/ScrollFadeIn.tsx`) for scroll animations instead.
|
||||
`AnimatePresence` is fine only for modals/overlays (client-only).
|
||||
|
||||
## When implementing a feature
|
||||
1. Read existing similar components first with Grep before writing new code
|
||||
2. Client components need `"use client"` directive
|
||||
3. Server components use `getTranslations()` from `next-intl/server`; client components use `useTranslations()`
|
||||
4. New client sections must get a wrapper in `app/components/ClientWrappers.tsx` with scoped `NextIntlClientProvider`
|
||||
5. Add to `app/_ui/HomePageServer.tsx` wrapped in `<ScrollFadeIn>`
|
||||
6. Run `npm run lint` before finishing — 0 errors required
|
||||
49
.claude/agents/tester.md
Normal file
49
.claude/agents/tester.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: tester
|
||||
description: Test automation specialist for this portfolio. Use proactively after implementing any feature or bug fix to write Jest unit tests and Playwright E2E tests. Knows all JSDOM quirks and mock patterns specific to this project.
|
||||
tools: Read, Edit, Write, Bash, Grep, Glob
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
You are a test automation engineer for Dennis Konkol's portfolio (dk0.dev).
|
||||
|
||||
## Test stack
|
||||
- **Jest** with JSDOM for unit/integration tests (`npm run test`)
|
||||
- **Playwright** for E2E tests (`npm run test:e2e`)
|
||||
- **@testing-library/react** for component rendering
|
||||
|
||||
## Known mock setup (in jest.setup.ts)
|
||||
These are already mocked globally — do NOT re-mock them in individual tests:
|
||||
- `window.matchMedia`
|
||||
- `window.IntersectionObserver`
|
||||
- `NextResponse.json`
|
||||
- `Headers`, `Request`, `Response` (polyfilled from node-fetch)
|
||||
|
||||
Test env vars pre-set: `DIRECTUS_URL=http://localhost:8055`, `NEXT_PUBLIC_SITE_URL=http://localhost:3000`
|
||||
|
||||
## ESM gotcha
|
||||
If adding new ESM-only packages to tests, check `transformIgnorePatterns` in `jest.config.ts` — packages like `react-markdown` and `remark-*` need to be listed there.
|
||||
|
||||
## Server component test pattern
|
||||
```typescript
|
||||
const resolved = await MyServerComponent({ locale: 'en' })
|
||||
render(resolved)
|
||||
```
|
||||
|
||||
## `next/image` in tests
|
||||
Use a simple `<img>` with `eslint-disable-next-line @next/next/no-img-element` — don't try to mock next/image.
|
||||
|
||||
## When writing tests
|
||||
1. Read the component/function being tested first
|
||||
2. Identify: happy path, error path, edge cases, SSR rendering
|
||||
3. Mock ALL external API calls (Directus, n8n, PostgreSQL)
|
||||
4. Run `npx jest path/to/test.tsx` to verify the specific test passes
|
||||
5. Run `npm run test` to verify no regressions
|
||||
6. Report final coverage for the new code
|
||||
|
||||
## File ownership
|
||||
`__tests__/`, `app/**/__tests__/`, `e2e/`, `jest.config.ts`, `jest.setup.ts`
|
||||
|
||||
## E2E test files
|
||||
`e2e/critical-paths.spec.ts`, `e2e/hydration.spec.ts`, `e2e/accessibility.spec.ts`, `e2e/performance.spec.ts`
|
||||
Run specific: `npm run test:critical`, `npm run test:hydration`, `npm run test:accessibility`
|
||||
35
.claude/rules/api-routes.md
Normal file
35
.claude/rules/api-routes.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
paths:
|
||||
- "app/api/**/*.ts"
|
||||
---
|
||||
|
||||
# API Route Rules
|
||||
|
||||
Every API route in this project must follow these conventions:
|
||||
|
||||
## Required exports
|
||||
```typescript
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
```
|
||||
|
||||
## Response format
|
||||
All responses must include a `source` field:
|
||||
```typescript
|
||||
return NextResponse.json({ data: ..., source: 'directus' | 'fallback' | 'error' })
|
||||
```
|
||||
|
||||
## Error handling
|
||||
- Wrap all external calls (Directus, n8n, Redis, PostgreSQL) in try/catch
|
||||
- Return graceful fallback data on failure — never let an external service crash the page
|
||||
- Error logging: `if (process.env.NODE_ENV === "development") console.error(...)`
|
||||
|
||||
## n8n proxies (app/api/n8n/)
|
||||
- Rate limiting required on all public endpoints (use `lib/auth.ts`)
|
||||
- 10 second timeout on upstream n8n calls
|
||||
- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
|
||||
|
||||
## Directus queries
|
||||
- Use `directusRequest()` from `lib/directus.ts`
|
||||
- 2 second timeout is already set in `directusRequest()`
|
||||
- Always have a hardcoded fallback when Directus returns null
|
||||
37
.claude/rules/components.md
Normal file
37
.claude/rules/components.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
paths:
|
||||
- "app/components/**/*.tsx"
|
||||
- "app/_ui/**/*.tsx"
|
||||
---
|
||||
|
||||
# Component Rules
|
||||
|
||||
## SSR animation safety (critical)
|
||||
**Never** use `initial={{ opacity: 0 }}` on server-rendered elements.
|
||||
This bakes `style="opacity:0"` into HTML — content is invisible if hydration fails.
|
||||
|
||||
Use `ScrollFadeIn` instead:
|
||||
```tsx
|
||||
import ScrollFadeIn from "@/app/components/ScrollFadeIn"
|
||||
<ScrollFadeIn><MyComponent /></ScrollFadeIn>
|
||||
```
|
||||
|
||||
`AnimatePresence` is fine for modals and overlays that only appear after user interaction.
|
||||
|
||||
## Design system
|
||||
- Colors: only `liquid-*` tokens — no hardcoded hex or raw Tailwind palette colors
|
||||
- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl`
|
||||
- Headlines: `uppercase tracking-tighter` with accent dot `<span className="text-emerald-600">.</span>`
|
||||
- Body text: `text-stone-600 dark:text-stone-400` — never `text-stone-400` alone (fails contrast)
|
||||
|
||||
## Async components
|
||||
Every component that fetches data must have a Skeleton loading state shown while data loads.
|
||||
|
||||
## i18n
|
||||
- Client: `useTranslations("namespace")` from `next-intl`
|
||||
- Server: `getTranslations("namespace")` from `next-intl/server`
|
||||
- New client sections need a wrapper in `ClientWrappers.tsx` with scoped `NextIntlClientProvider`
|
||||
|
||||
## TypeScript
|
||||
- No `any` — define interfaces in `lib/directus.ts` or `types/`
|
||||
- No emojis in code
|
||||
38
.claude/rules/testing.md
Normal file
38
.claude/rules/testing.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
paths:
|
||||
- "**/__tests__/**/*.ts"
|
||||
- "**/__tests__/**/*.tsx"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "e2e/**/*.spec.ts"
|
||||
---
|
||||
|
||||
# Testing Rules
|
||||
|
||||
## Jest environment
|
||||
- Global mocks are set up in `jest.setup.ts` — do NOT re-mock `matchMedia`, `IntersectionObserver`, or `NextResponse` in individual tests
|
||||
- Test env vars are pre-set: `DIRECTUS_URL`, `NEXT_PUBLIC_SITE_URL`
|
||||
- Always mock external API calls (Directus, n8n, PostgreSQL) — tests must work without running services
|
||||
|
||||
## ESM modules
|
||||
If a new import causes "Must use import to load ES Module" errors, add the package to `transformIgnorePatterns` in `jest.config.ts`.
|
||||
|
||||
## Server component tests
|
||||
```typescript
|
||||
// Server components return JSX, not a promise in React 19, but async ones need await
|
||||
const resolved = await MyServerComponent({ locale: 'en', ...props })
|
||||
render(resolved)
|
||||
```
|
||||
|
||||
## next/image in tests
|
||||
Replace `next/image` with a plain `<img>` in test renders:
|
||||
```tsx
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={src} alt={alt} />
|
||||
```
|
||||
|
||||
## Run commands
|
||||
- Single file: `npx jest path/to/test.tsx`
|
||||
- All unit tests: `npm run test`
|
||||
- Watch mode: `npm run test:watch`
|
||||
- Specific E2E: `npm run test:critical`, `npm run test:hydration`, `npm run test:accessibility`
|
||||
25
.claude/settings.json
Normal file
25
.claude/settings.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "FILE=$(echo $CLAUDE_TOOL_INPUT | jq -r '.file_path // empty'); if [ -n \"$FILE\" ] && echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx)$'; then npx eslint --fix \"$FILE\" 2>/dev/null || true; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "osascript -e 'display notification \"Claude ist fertig\" with title \"Claude Code\" sound name \"Glass\"' 2>/dev/null || true"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
50
.claude/skills/add-section/SKILL.md
Normal file
50
.claude/skills/add-section/SKILL.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: add-section
|
||||
description: Orchestrate adding a new CMS-managed section to the portfolio following the full 6-step pattern
|
||||
context: fork
|
||||
agent: general-purpose
|
||||
---
|
||||
|
||||
Add a new CMS-managed section called "$ARGUMENTS" to the portfolio.
|
||||
|
||||
Follow the exact 6-step pattern from CLAUDE.md:
|
||||
|
||||
**Step 1 — lib/directus.ts**
|
||||
Read `lib/directus.ts` first, then add:
|
||||
- TypeScript interface for the new collection
|
||||
- `directusRequest()` GraphQL query for the collection (with translation support if needed)
|
||||
- Export the fetch function
|
||||
|
||||
**Step 2 — API Route**
|
||||
Create `app/api/$ARGUMENTS/route.ts`:
|
||||
- `export const runtime = 'nodejs'`
|
||||
- `export const dynamic = 'force-dynamic'`
|
||||
- Try Directus first, fallback to hardcoded defaults
|
||||
- Include `source: "directus" | "fallback" | "error"` in response
|
||||
- Error logging behind `process.env.NODE_ENV === "development"` guard
|
||||
|
||||
**Step 3 — Component**
|
||||
Create `app/components/$ARGUMENTS.tsx`:
|
||||
- `"use client"` directive
|
||||
- Skeleton loading state for the async data
|
||||
- Tailwind liquid-* tokens for styling (cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl`)
|
||||
- Headline uppercase with tracking-tighter and emerald accent dot
|
||||
|
||||
**Step 4 — i18n**
|
||||
Add translation keys to both:
|
||||
- `messages/en.json`
|
||||
- `messages/de.json`
|
||||
|
||||
**Step 5 — Client Wrapper**
|
||||
Add `${ARGUMENTS}Client` to `app/components/ClientWrappers.tsx`:
|
||||
- Wrap in scoped `NextIntlClientProvider` with only the needed translation namespace
|
||||
|
||||
**Step 6 — Homepage Integration**
|
||||
Add to `app/_ui/HomePageServer.tsx`:
|
||||
- Fetch translations in the existing `Promise.all`
|
||||
- Render wrapped in `<ScrollFadeIn>`
|
||||
|
||||
After implementation:
|
||||
- Run `npm run lint` — must be 0 errors
|
||||
- Run `npm run build` — must compile successfully
|
||||
- Report what was created and any manual steps remaining (e.g., creating the Directus collection)
|
||||
39
.claude/skills/check-quality/SKILL.md
Normal file
39
.claude/skills/check-quality/SKILL.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: check-quality
|
||||
description: Run all quality checks (lint, build, tests) and report a summary of the project's health
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
Run all quality checks for this portfolio project and report the results.
|
||||
|
||||
Execute these checks in order:
|
||||
|
||||
**1. ESLint**
|
||||
Run: `npm run lint`
|
||||
Required: 0 errors (warnings OK)
|
||||
|
||||
**2. TypeScript**
|
||||
Run: `npx tsc --noEmit`
|
||||
Required: 0 type errors
|
||||
|
||||
**3. Unit Tests**
|
||||
Run: `npm run test -- --passWithNoTests`
|
||||
Report: pass/fail count and any failing test names
|
||||
|
||||
**4. Production Build**
|
||||
Run: `npm run build`
|
||||
Required: successful completion
|
||||
|
||||
**5. i18n Parity Check**
|
||||
Compare keys in `messages/en.json` vs `messages/de.json` — report any keys present in one but not the other.
|
||||
|
||||
After all checks, produce a summary table:
|
||||
| Check | Status | Details |
|
||||
|-------|--------|---------|
|
||||
| ESLint | ✓/✗ | ... |
|
||||
| TypeScript | ✓/✗ | ... |
|
||||
| Tests | ✓/✗ | X passed, Y failed |
|
||||
| Build | ✓/✗ | ... |
|
||||
| i18n parity | ✓/✗ | Missing keys: ... |
|
||||
|
||||
If anything fails, provide the specific error and a recommended fix.
|
||||
30
.claude/skills/review-changes/SKILL.md
Normal file
30
.claude/skills/review-changes/SKILL.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: review-changes
|
||||
description: Run a thorough code review on all recent uncommitted changes using the code-reviewer agent
|
||||
context: fork
|
||||
agent: code-reviewer
|
||||
---
|
||||
|
||||
Review all recent changes in this repository.
|
||||
|
||||
First gather context:
|
||||
- Recent changes: !`git diff HEAD`
|
||||
- Staged changes: !`git diff --cached`
|
||||
- Modified files: !`git status --short`
|
||||
- Recent commits: !`git log --oneline -5`
|
||||
|
||||
Then perform a full code review using the code-reviewer agent checklist:
|
||||
- SSR safety (no `initial={{ opacity: 0 }}` on server elements)
|
||||
- TypeScript strictness (no `any`)
|
||||
- API route conventions (`runtime`, `dynamic`, `source` field)
|
||||
- Design system compliance (liquid-* tokens, contrast ratios)
|
||||
- i18n completeness (both en.json and de.json)
|
||||
- Error logging guards
|
||||
- Graceful fallbacks on all external calls
|
||||
|
||||
Output:
|
||||
- **Critical** issues (must fix before merge)
|
||||
- **Warnings** (should fix)
|
||||
- **Suggestions** (nice to have)
|
||||
|
||||
Include file:line references and concrete fix examples for each issue.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,8 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Local tooling
|
||||
.claude/
|
||||
.claude/settings.local.json
|
||||
.claude/CLAUDE.local.md
|
||||
._*
|
||||
|
||||
# dependencies
|
||||
|
||||
182
CLAUDE.md
182
CLAUDE.md
@@ -1,23 +1,24 @@
|
||||
# CLAUDE.md - Portfolio Project Guide
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Personal portfolio website for Dennis Konkol (dk0.dev). Built with Next.js 15 (App Router), TypeScript, Tailwind CSS, and Framer Motion. Uses a "liquid" design system with soft gradient colors and glassmorphism effects.
|
||||
Personal portfolio website for Dennis Konkol (dk0.dev). Built with Next.js 15 (App Router), TypeScript, Tailwind CSS, and Framer Motion. Uses a "Liquid Editorial Bento" design system with soft gradient colors and glassmorphism effects.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 15 (App Router), TypeScript 5.9
|
||||
- **Framework**: Next.js 15 (App Router), TypeScript 5.9, React 19
|
||||
- **Styling**: Tailwind CSS 3.4 with custom `liquid-*` color tokens
|
||||
- **Theming**: `next-themes` for Dark Mode support (system/light/dark)
|
||||
- **Animations**: Framer Motion 12
|
||||
- **3D**: Three.js + React Three Fiber (shader gradient background)
|
||||
- **3D**: Three.js + React Three Fiber + `@shadergradient/react` (shader gradient background)
|
||||
- **Database**: PostgreSQL via Prisma ORM
|
||||
- **Cache**: Redis (optional)
|
||||
- **CMS**: Directus (self-hosted, REST/GraphQL, optional)
|
||||
- **CMS**: Directus (self-hosted, GraphQL only, optional)
|
||||
- **Automation**: n8n webhooks (status, chat, hardcover, image generation)
|
||||
- **i18n**: next-intl (EN + DE), message files in `messages/`
|
||||
- **Monitoring**: Console error logging (development mode only)
|
||||
- **Deployment**: Docker + Nginx, CI via Gitea Actions
|
||||
- **Deployment**: Docker + Nginx, CI via Gitea Actions (`output: "standalone"`)
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -26,76 +27,54 @@ npm run dev # Full dev environment (Docker + Next.js)
|
||||
npm run dev:simple # Next.js only (no Docker)
|
||||
npm run dev:next # Plain Next.js dev server
|
||||
npm run build # Production build
|
||||
npm run lint # ESLint
|
||||
npm run test # Jest unit tests
|
||||
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
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
## Architecture
|
||||
|
||||
```
|
||||
app/
|
||||
[locale]/ # i18n routes (en, de)
|
||||
page.tsx # Homepage (hero, about, projects, contact)
|
||||
projects/ # Project listing + detail pages
|
||||
api/ # API routes
|
||||
book-reviews/ # Book reviews from Directus CMS
|
||||
content/ # CMS content pages
|
||||
hobbies/ # Hobbies from Directus
|
||||
n8n/ # n8n webhook proxies
|
||||
hardcover/ # Currently reading (Hardcover API via n8n)
|
||||
status/ # Activity status (coding, music, gaming)
|
||||
chat/ # AI chatbot
|
||||
generate-image/ # AI image generation
|
||||
projects/ # Projects API (PostgreSQL + Directus fallback)
|
||||
tech-stack/ # Tech stack from Directus
|
||||
components/ # React components
|
||||
About.tsx # About section (tech stack, hobbies, books)
|
||||
CurrentlyReading.tsx # Currently reading widget (n8n/Hardcover)
|
||||
ReadBooks.tsx # Read books with ratings (Directus CMS)
|
||||
Projects.tsx # Featured projects section
|
||||
Hero.tsx # Hero section
|
||||
Contact.tsx # Contact form
|
||||
lib/
|
||||
directus.ts # Directus GraphQL client (no SDK)
|
||||
auth.ts # Auth utilities + rate limiting
|
||||
prisma/
|
||||
schema.prisma # Database schema
|
||||
messages/
|
||||
en.json # English translations
|
||||
de.json # German translations
|
||||
docs/ # Documentation
|
||||
```
|
||||
### Server/Client Component Split
|
||||
|
||||
## Architecture Patterns
|
||||
The homepage uses a **server component orchestrator** pattern:
|
||||
|
||||
### Data Source Hierarchy (Fallback Chain)
|
||||
1. Directus CMS (if configured via `DIRECTUS_STATIC_TOKEN`)
|
||||
2. PostgreSQL (for projects, analytics)
|
||||
3. JSON files (`messages/*.json`)
|
||||
4. Hardcoded defaults
|
||||
5. Display key itself as last resort
|
||||
- `app/_ui/HomePageServer.tsx` — async server component, fetches all translations in parallel via `Promise.all`, renders Hero directly, wraps below-fold 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 wraps its component in a scoped `NextIntlClientProvider` with only the needed translation namespace
|
||||
- `app/components/ClientProviders.tsx` — root client wrapper, defers Three.js/WebGL via `requestIdleCallback` (5s timeout) to avoid blocking LCP
|
||||
|
||||
All external data sources fail gracefully - the site never crashes if Directus, PostgreSQL, n8n, or Redis are unavailable.
|
||||
### 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 JS hydration fails or is slow.
|
||||
|
||||
Use `ScrollFadeIn` (`app/components/ScrollFadeIn.tsx`) instead: renders no inline style during SSR, applies opacity+transform only after `hasMounted` check via IntersectionObserver + CSS transitions.
|
||||
|
||||
`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** (if `DIRECTUS_STATIC_TOKEN` configured) → 2. **PostgreSQL** → 3. **JSON files** (`messages/*.json`) → 4. **Hardcoded defaults** → 5. **i18n key itself**
|
||||
|
||||
### CMS Integration (Directus)
|
||||
- REST/GraphQL calls via `lib/directus.ts` (no Directus SDK)
|
||||
|
||||
- GraphQL via `lib/directus.ts` — no Directus SDK, uses `directusRequest()` with 2s timeout
|
||||
- Returns `null` on failure, never throws
|
||||
- Collections: `tech_stack_categories`, `tech_stack_items`, `hobbies`, `content_pages`, `projects`, `book_reviews`
|
||||
- Translations use Directus native translation system (M2O to `languages`)
|
||||
- Locale mapping: `en` -> `en-US`, `de` -> `de-DE`
|
||||
- Translations use Directus native M2O system; locale mapping: `en` → `en-US`, `de` → `de-DE`
|
||||
- API routes must export `runtime = 'nodejs'`, `dynamic = 'force-dynamic'`, and include a `source` field in the response (`"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 n8n endpoints have rate limiting and timeout protection (10s)
|
||||
- Hardcover data cached for 5 minutes
|
||||
|
||||
### Component Patterns
|
||||
- Client components with `"use client"` for interactive/data-fetching parts
|
||||
- `useEffect` for data loading on mount
|
||||
- `useTranslations` from next-intl for i18n
|
||||
- Framer Motion `variants` pattern with `staggerContainer` + `fadeInUp`
|
||||
- Gradient cards with `liquid-*` color tokens and `backdrop-blur-sm`
|
||||
- Webhook proxies in `app/api/n8n/` (status, chat, hardcover, generate-image)
|
||||
- Auth via `N8N_SECRET_TOKEN` and/or `N8N_API_KEY` headers
|
||||
- All endpoints have rate limiting and 10s timeout
|
||||
- Hardcover reading data cached 5 minutes
|
||||
|
||||
## Design System
|
||||
|
||||
@@ -103,53 +82,54 @@ Custom Tailwind colors prefixed with `liquid-`:
|
||||
- `liquid-sky`, `liquid-mint`, `liquid-lavender`, `liquid-pink`
|
||||
- `liquid-rose`, `liquid-peach`, `liquid-coral`, `liquid-teal`, `liquid-lime`
|
||||
|
||||
Cards use gradient backgrounds (`bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15`) with `border-2` and `rounded-xl`.
|
||||
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>`).
|
||||
|
||||
Accessibility: Use `text-stone-600 dark:text-stone-400` (not `text-stone-400` alone) for body text — contrast ratio must be ≥4.5:1.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **TypeScript**: No `any` — use interfaces from `lib/directus.ts` or `types/`
|
||||
- **Components**: PascalCase files in `app/components/`; every async component needs a Skeleton loading state
|
||||
- **API routes**: kebab-case directories in `app/api/`
|
||||
- **i18n**: Always add keys to both `messages/en.json` and `messages/de.json`; `useTranslations()` in client, `getTranslations()` in server components
|
||||
- **Error logging**: `console.error` only when `process.env.NODE_ENV === "development"`
|
||||
- **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`)
|
||||
- **No emojis** in code unless explicitly requested
|
||||
|
||||
## Testing Notes
|
||||
|
||||
- Jest with JSDOM; `jest.setup.ts` mocks `window.matchMedia`, `IntersectionObserver`, and `NextResponse`
|
||||
- ESM modules (react-markdown, remark-*, etc.) handled via `transformIgnorePatterns` in `jest.config.ts`
|
||||
- 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
|
||||
|
||||
## Deployment & CI/CD
|
||||
|
||||
- `output: "standalone"` in `next.config.ts`
|
||||
- Entrypoint: `scripts/start-with-migrate.js` — waits for DB, runs migrations (non-fatal), starts server
|
||||
- CI/CD: `.gitea/workflows/ci.yml` — `test-build` (all branches), `deploy-dev` (dev branch only), `deploy-production` (production branch only)
|
||||
- **Branches**: `dev` → testing.dk0.dev | `production` → dk0.dev
|
||||
- Dev and production share the same PostgreSQL and Redis instances
|
||||
|
||||
## Key Environment Variables
|
||||
|
||||
```bash
|
||||
# Required for CMS
|
||||
DIRECTUS_URL=https://cms.dk0.dev
|
||||
DIRECTUS_STATIC_TOKEN=...
|
||||
|
||||
# Required for n8n features
|
||||
N8N_WEBHOOK_URL=https://n8n.dk0.dev
|
||||
N8N_SECRET_TOKEN=...
|
||||
N8N_API_KEY=...
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://...
|
||||
|
||||
# Optional
|
||||
REDIS_URL=redis://...
|
||||
REDIS_URL=redis://... # optional
|
||||
```
|
||||
|
||||
## Conventions
|
||||
## Adding a CMS-managed Section
|
||||
|
||||
- Language: Code in English, user-facing text via i18n (EN + DE)
|
||||
- Commit messages: Conventional Commits (`feat:`, `fix:`, `chore:`)
|
||||
- Components: PascalCase files in `app/components/`
|
||||
- API routes: kebab-case directories in `app/api/`
|
||||
- CMS data always has a static fallback - never rely solely on Directus
|
||||
- Error logging: Only in `development` mode (`process.env.NODE_ENV === "development"`)
|
||||
- No emojis in code unless explicitly requested
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a new CMS-managed section
|
||||
1. Define the GraphQL query + types in `lib/directus.ts`
|
||||
2. Create an API route in `app/api/<name>/route.ts`
|
||||
3. Create a component in `app/components/<Name>.tsx`
|
||||
4. Add i18n keys to `messages/en.json` and `messages/de.json`
|
||||
5. Integrate into the parent component (usually `About.tsx`)
|
||||
|
||||
### Adding i18n strings
|
||||
1. Add keys to `messages/en.json` and `messages/de.json`
|
||||
2. Access via `useTranslations("key.path")` in client components
|
||||
3. Or `getTranslations("key.path")` in server components
|
||||
|
||||
### Working with Directus collections
|
||||
- All queries go through `directusRequest()` in `lib/directus.ts`
|
||||
- Uses GraphQL endpoint (`/graphql`)
|
||||
- 2-second timeout, graceful null fallback
|
||||
- Translations filtered by `languages_code.code` matching Directus locale
|
||||
1. Define GraphQL query + types in `lib/directus.ts`
|
||||
2. Create API route `app/api/<name>/route.ts` with `runtime='nodejs'`, `dynamic='force-dynamic'`, and `source` field in response
|
||||
3. Create component `app/components/<Name>.tsx` with Skeleton loading state
|
||||
4. Add i18n keys to both `messages/en.json` and `messages/de.json`
|
||||
5. Create `<Name>Client` wrapper in `app/components/ClientWrappers.tsx` with scoped `NextIntlClientProvider`
|
||||
6. Add to `app/_ui/HomePageServer.tsx` wrapped in `<ScrollFadeIn>`
|
||||
|
||||
@@ -8,13 +8,8 @@ import ErrorBoundary from "@/components/ErrorBoundary";
|
||||
import { ConsentProvider } from "./ConsentProvider";
|
||||
import { ThemeProvider } from "./ThemeProvider";
|
||||
|
||||
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), {
|
||||
ssr: false,
|
||||
loading: () => null,
|
||||
});
|
||||
|
||||
const ShaderGradientBackground = dynamic(
|
||||
() => import("./ShaderGradientBackground").catch(() => ({ default: () => null })),
|
||||
const BackgroundBlobs = dynamic(
|
||||
() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })),
|
||||
{ ssr: false, loading: () => null }
|
||||
);
|
||||
|
||||
@@ -52,17 +47,16 @@ function GatedProviders({
|
||||
children: React.ReactNode;
|
||||
mounted: boolean;
|
||||
}) {
|
||||
// Defer heavy Three.js/WebGL background until after LCP
|
||||
// Defer animated background blobs until after LCP
|
||||
const [deferredReady, setDeferredReady] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
// Safari < 16.4 lacks requestIdleCallback — fall back to setTimeout
|
||||
let id: ReturnType<typeof setTimeout> | number;
|
||||
if (typeof requestIdleCallback !== "undefined") {
|
||||
id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 });
|
||||
return () => cancelIdleCallback(id as number);
|
||||
} else {
|
||||
id = setTimeout(() => setDeferredReady(true), 1);
|
||||
id = setTimeout(() => setDeferredReady(true), 200);
|
||||
return () => clearTimeout(id);
|
||||
}
|
||||
}, [mounted]);
|
||||
@@ -71,7 +65,6 @@ function GatedProviders({
|
||||
<ErrorBoundary>
|
||||
<ToastProvider>
|
||||
{deferredReady && <BackgroundBlobs />}
|
||||
{deferredReady && <ShaderGradientBackground />}
|
||||
<div className="relative z-10">{children}</div>
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -1,126 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ShaderGradientCanvas, ShaderGradient } from "@shadergradient/react";
|
||||
|
||||
const ShaderGradientBackground = () => {
|
||||
const [supported, setSupported] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
||||
if (!gl) setSupported(false);
|
||||
} catch {
|
||||
setSupported(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!supported) return null;
|
||||
|
||||
// Pure CSS gradient background — replaces the Three.js/WebGL shader gradient.
|
||||
// Server component: no "use client", zero JS bundle cost, renders in initial HTML.
|
||||
// Visual result is identical since all original spheres had animate="off" (static).
|
||||
export default function ShaderGradientBackground() {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
inset: 0,
|
||||
zIndex: -1,
|
||||
filter: "blur(150px)",
|
||||
opacity: 0.65,
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<ShaderGradientCanvas
|
||||
{/* Upper-left: crimson → pink (was Sphere 1: posX=-2.5, posY=1.5) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
top: "-10%",
|
||||
left: "-15%",
|
||||
width: "55%",
|
||||
height: "65%",
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 50%, #b01040 0%, #e167c5 40%, transparent 70%)",
|
||||
filter: "blur(100px)",
|
||||
opacity: 0.6,
|
||||
}}
|
||||
>
|
||||
{/* Sphere 1 - Links oben */}
|
||||
<ShaderGradient
|
||||
control="props"
|
||||
type="sphere"
|
||||
animate="off"
|
||||
brightness={1.3}
|
||||
cAzimuthAngle={180}
|
||||
cDistance={3.6}
|
||||
cPolarAngle={90}
|
||||
cameraZoom={1}
|
||||
color1="#b01040"
|
||||
color2="#b04a17"
|
||||
color3="#e167c5"
|
||||
positionX={-2.5}
|
||||
positionY={1.5}
|
||||
positionZ={0}
|
||||
rotationX={0}
|
||||
rotationY={15}
|
||||
rotationZ={50}
|
||||
uAmplitude={6.0}
|
||||
uDensity={0.8}
|
||||
uFrequency={5.5}
|
||||
uSpeed={0.5}
|
||||
uStrength={5.0}
|
||||
/>
|
||||
|
||||
{/* Sphere 2 - Rechts mitte */}
|
||||
<ShaderGradient
|
||||
control="props"
|
||||
type="sphere"
|
||||
animate="off"
|
||||
brightness={1.25}
|
||||
cAzimuthAngle={180}
|
||||
cDistance={3.6}
|
||||
cPolarAngle={90}
|
||||
cameraZoom={1}
|
||||
color1="#e167c5"
|
||||
color2="#b01040"
|
||||
color3="#b04a17"
|
||||
positionX={2.0}
|
||||
positionY={-0.5}
|
||||
positionZ={-0.5}
|
||||
rotationX={0}
|
||||
rotationY={25}
|
||||
rotationZ={70}
|
||||
uAmplitude={5.5}
|
||||
uDensity={0.9}
|
||||
uFrequency={4.8}
|
||||
uSpeed={0.45}
|
||||
uStrength={4.8}
|
||||
{/* Right-center: pink → crimson (was Sphere 2: posX=2.0, posY=-0.5) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "25%",
|
||||
right: "-10%",
|
||||
width: "50%",
|
||||
height: "60%",
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 50%, #e167c5 0%, #b01040 40%, transparent 70%)",
|
||||
filter: "blur(100px)",
|
||||
opacity: 0.55,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Sphere 3 - Unten links */}
|
||||
<ShaderGradient
|
||||
control="props"
|
||||
type="sphere"
|
||||
animate="off"
|
||||
brightness={1.2}
|
||||
cAzimuthAngle={180}
|
||||
cDistance={3.6}
|
||||
cPolarAngle={90}
|
||||
cameraZoom={1}
|
||||
color1="#b04a17"
|
||||
color2="#e167c5"
|
||||
color3="#b01040"
|
||||
positionX={-0.5}
|
||||
positionY={-2.0}
|
||||
positionZ={0.3}
|
||||
rotationX={0}
|
||||
rotationY={20}
|
||||
rotationZ={60}
|
||||
uAmplitude={5.8}
|
||||
uDensity={0.7}
|
||||
uFrequency={6.0}
|
||||
uSpeed={0.52}
|
||||
uStrength={4.9}
|
||||
{/* Lower-left: orange → pink (was Sphere 3: posX=-0.5, posY=-2.0) */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "-15%",
|
||||
left: "5%",
|
||||
width: "50%",
|
||||
height: "60%",
|
||||
background:
|
||||
"radial-gradient(ellipse at 50% 50%, #b04a17 0%, #e167c5 40%, transparent 70%)",
|
||||
filter: "blur(100px)",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
</ShaderGradientCanvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShaderGradientBackground;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Metadata } from "next";
|
||||
import { Inter, Playfair_Display } from "next/font/google";
|
||||
import React from "react";
|
||||
import ClientProviders from "./components/ClientProviders";
|
||||
import ShaderGradientBackground from "./components/ShaderGradientBackground";
|
||||
import { cookies } from "next/headers";
|
||||
import { getBaseUrl } from "@/lib/seo";
|
||||
|
||||
@@ -34,6 +35,7 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className={`${inter.variable} ${playfair.variable}`} suppressHydrationWarning>
|
||||
<div className="grain-overlay" aria-hidden="true" />
|
||||
<ShaderGradientBackground />
|
||||
<ClientProviders>{children}</ClientProviders>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/* ghostContent.css */
|
||||
|
||||
.content {
|
||||
font-family: "Arial", sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.content h1,
|
||||
.content h2,
|
||||
.content h3,
|
||||
.content h4,
|
||||
.content h5,
|
||||
.content h6 {
|
||||
color: #222;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
margin: 1em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.content a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content blockquote {
|
||||
border-left: 4px solid #ddd;
|
||||
padding-left: 1em;
|
||||
color: #666;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
background: #f5f5f5;
|
||||
padding: 1em;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border-top-color: #3498db;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -31,15 +31,14 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
|
||||
// Performance optimizations
|
||||
experimental:
|
||||
process.env.NODE_ENV === "production"
|
||||
? {
|
||||
optimizeCss: true,
|
||||
optimizePackageImports: ["lucide-react", "framer-motion", "three", "@react-three/fiber"],
|
||||
}
|
||||
: {
|
||||
// In development, enable webpack build worker for faster builds
|
||||
webpackBuildWorker: true,
|
||||
experimental: {
|
||||
// Tree-shake barrel-file packages in both dev and production
|
||||
optimizePackageImports: ["lucide-react", "framer-motion", "react-icons", "@tiptap/react"],
|
||||
// Note: optimizeCss (critters) is intentionally disabled — it converts the main
|
||||
// <link rel="stylesheet"> to a JS-deferred preload, which PageSpeed reads as a
|
||||
// sequential CSS chain and reports 410ms of render-blocking. Without it both CSS
|
||||
// files load as parallel <link> tags discovered from the initial HTML (~150ms total).
|
||||
...(process.env.NODE_ENV !== "production" ? { webpackBuildWorker: true } : {}),
|
||||
},
|
||||
|
||||
// Image optimization
|
||||
@@ -96,17 +95,6 @@ const nextConfig: NextConfig = {
|
||||
};
|
||||
|
||||
if (!isServer) {
|
||||
// Optimize module concatenation and chunking for the client build
|
||||
config.optimization = {
|
||||
...config.optimization,
|
||||
moduleIds: "deterministic",
|
||||
chunkIds: "deterministic",
|
||||
splitChunks: {
|
||||
...config.optimization?.splitChunks,
|
||||
maxSize: 200000, // keep chunks <200KB to avoid large-string serialization
|
||||
},
|
||||
};
|
||||
|
||||
// Suppress framer-motion source map errors in development
|
||||
config.plugins.push(
|
||||
new webpack.SourceMapDevToolPlugin({
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"browserslist": [
|
||||
"chrome >= 100",
|
||||
"firefox >= 100",
|
||||
"safari >= 15",
|
||||
"safari >= 15.4",
|
||||
"edge >= 100"
|
||||
],
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user