diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3975569 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,211 @@ +# 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. + +## 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 +``` + +### Build & Deploy +```bash +npm run build # Production build (standalone mode) +npm run start # Start production server +``` + +### 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 + +# 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 +``` + +### Linting +```bash +npm run lint # Run ESLint +npm run lint:fix # Auto-fix issues +``` + +### 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 +``` + +## Architecture Overview + +### 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**: Sentry +- **Deployment**: Docker (standalone mode) + Nginx + +### 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 +``` + +### 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. + +### 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`) +- 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`) + +### 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 + +## 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` + +### 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 + +### 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) + +### 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`) + +### 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 + +### 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 + +## 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 +SENTRY_DSN=... +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` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e34561b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,156 @@ +# CLAUDE.md - Portfolio Project Guide + +## 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. + +## 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 support (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, REST/GraphQL, optional) +- **Automation**: n8n webhooks (status, chat, hardcover, image generation) +- **i18n**: next-intl (EN + DE), message files in `messages/` +- **Monitoring**: Sentry +- **Deployment**: Docker + Nginx, CI via Gitea Actions + +## Commands + +```bash +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 test:e2e # Playwright E2E tests +``` + +## Project Structure + +``` +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 +``` + +## Architecture Patterns + +### 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 + +All external data sources fail gracefully - the site never crashes if Directus, PostgreSQL, n8n, or Redis are unavailable. + +### CMS Integration (Directus) +- REST/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 translation system (M2O to `languages`) +- Locale mapping: `en` -> `en-US`, `de` -> `de-DE` + +### 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` + +## Design System + +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`. + +## 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://... +SENTRY_DSN=... +``` + +## Conventions + +- 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//route.ts` +3. Create a component in `app/components/.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 diff --git a/DIRECTUS_MIGRATION.md b/DIRECTUS_MIGRATION.md deleted file mode 100644 index c2810e3..0000000 --- a/DIRECTUS_MIGRATION.md +++ /dev/null @@ -1,146 +0,0 @@ -# Directus Integration - Migration Guide - -## 🎯 Overview - -This portfolio now has a **hybrid i18n system**: -- ✅ **JSON Files** (Primary) → All translations work from `messages/*.json` files -- ✅ **Directus CMS** (Optional) → Can override translations dynamically without rebuilds - -**Important**: Directus is **optional**. The app works perfectly fine without it using JSON fallbacks. - -## 📁 New File Structure - -### Core Infrastructure -- `lib/directus.ts` - REST Client for Directus (uses `de-DE`, `en-US` locale codes) -- `lib/i18n-loader.ts` - Loads texts with Fallback Chain -- `lib/translations-loader.ts` - Batch loader for all sections (cleaned up to match actual usage) -- `types/translations.ts` - TypeScript types for all translation objects (fixed to match components) - -### Components -All component wrappers properly load and pass translations to client components. - -## 🔄 How It Works - -### Without Directus (Default) -``` -Component → useTranslations("nav") → JSON File (messages/en.json) -``` - -### With Directus (Optional) -``` -Server Component → getNavTranslations(locale) - → Try Directus API (de-DE/en-US) - → If not found: JSON File (de/en) - → Props to Client Component -``` - -## 🗄️ Directus Setup (Optional) - -Only set this up if you want to edit translations through a CMS without rebuilding the app. - -### 1. Environment Variables - -Add to `.env.local`: -```bash -DIRECTUS_URL=https://cms.example.com -DIRECTUS_STATIC_TOKEN=your_token_here -``` - -**If these are not set**, the system will skip Directus and use JSON files only. - -### 2. Collection: `messages` - -Create a `messages` collection in Directus with these fields: -- `key` (String, required) - e.g., "nav.home" -- `translations` (Translations) - Directus native translations feature -- Configure languages: `en-US` and `de-DE` - -**Note**: Keys use dot notation (`nav.home`) but locales use dashes (`en-US`, `de-DE`). - -### 3. Permissions - -Grant **Public** role read access to `messages` collection. - -## 📝 Translation Keys - -See `docs/LOCALE_SYSTEM.md` for the complete list of translation keys and their structure. - -All keys are organized hierarchically: -- `nav.*` - Navigation items -- `home.hero.*` - Hero section -- `home.about.*` - About section -- `home.projects.*` - Projects section -- `home.contact.*` - Contact form and info -- `footer.*` - Footer content -- `consent.*` - Privacy consent banner - -## 🎨 Rich Text Content - -For longer content that needs formatting (bold, italic, lists), use the `content_pages` collection: - -### Collection: `content_pages` (Optional) - -Fields: -- `slug` (String, unique) - e.g., "home-hero" -- `locale` (String) - `en` or `de` -- `title` (String) -- `content` (Rich Text or Long Text) - -Examples: -- `home-hero` - Hero section description -- `home-about` - About section content -- `home-contact` - Contact intro text - -Components fetch these via `/api/content/page` and render using `RichTextClient`. - -## 🔍 Fallback Chain - -For every translation key, the system searches in this order: - -1. **Directus** (if configured) in requested locale (e.g., `de-DE`) -2. **Directus** in English fallback (e.g., `en-US`) -3. **JSON file** in requested locale (e.g., `messages/de.json`) -4. **JSON file** in English (e.g., `messages/en.json`) -5. **Key itself** as last resort (e.g., returns `"nav.home"`) - -## ✅ What Was Fixed - -Previous issues that have been resolved: - -1. ✅ **Type mismatches** - All translation types now match actual component usage -2. ✅ **Unused fields** - Removed translation keys that were never used (like `hero.greeting`, `hero.name`) -3. ✅ **Wrong structure** - Fixed `AboutTranslations` structure (removed fake `interests` nesting) -4. ✅ **Missing keys** - Aligned loaders with JSON files and actual component requirements -5. ✅ **Confusing comments** - Removed misleading comments in `translations-loader.ts` - -## 🎯 Best Practices - -1. **Always maintain JSON files** - Even if using Directus, keep JSON files as fallback -2. **Use types** - TypeScript types ensure correct usage -3. **Test without Directus** - App should work perfectly without CMS configured -4. **Rich text for formatting** - Use `content_pages` for content that needs bold/italic/lists -5. **JSON for UI labels** - Use JSON/messages for short UI strings like buttons and labels - -## 🐛 Troubleshooting - -### Directus not configured -**This is normal!** The app works fine. All translations come from JSON files. - -### Want to use Directus? -1. Set up `DIRECTUS_URL` and `DIRECTUS_STATIC_TOKEN` -2. Create `messages` collection -3. Add your translations -4. They will override JSON values - -### Translation not showing? -Check in this order: -1. Does key exist in `messages/en.json`? -2. Is the key spelled correctly? -3. Is component using correct namespace? - -## 📚 Further Reading - -- **Complete locale documentation**: `docs/LOCALE_SYSTEM.md` -- **Directus setup checklist**: `DIRECTUS_CHECKLIST.md` -- **Operations guide**: `docs/OPERATIONS.md` - diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..74cd468 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,34 @@ +# Gemini CLI: Project Context & Engineering Mandates + +## Project Identity +- **Name:** Dennis Konkol Portfolio (dk0.dev) +- **Aesthetic:** "Liquid Editorial Bento" (Premium, minimalistisch, hoch-typografisch). +- **Core Palette:** Creme (`#fdfcf8`), Stone (`#0c0a09`), Emerald (`#10b981`), Sky, Purple. + +## Tech Stack +- **Framework:** Next.js 15 (App Router), Tailwind CSS 3.4. +- **CMS:** Directus (primär für Texte, Hobbies, Tech-Stack, Projekte). +- **Database:** PostgreSQL (Prisma) als lokaler Cache/Mirror für Projekte. +- **Animations:** Framer Motion (bevorzugt für alle Übergänge). +- **i18n:** `next-intl` (Locales: `en`, `de`). + +## Engineering Guidelines (Mandates) + +### 1. UI Components +- **Bento Grid:** Neue Features sollten immer in das bestehende Grid integriert werden. Keine schwebenden Overlays. +- **Skeletons:** Jede asynchrone Komponente benötigt einen passenden `Skeleton` Ladezustand. +- **Typography:** Headlines immer uppercase, tracking-tighter, mit Akzent-Punkt am Ende. + +### 2. Implementation Rules +- **TypeScript:** Keine `any`. Nutze bestehende Interfaces in `lib/directus.ts` oder `app/_ui/`. +- **Resilience:** Alle API-Calls müssen Fehler abfangen und sinnvolle Fallbacks (oder Skeletons) anzeigen. +- **Next.js Standalone:** Das Projekt nutzt den `standalone` Build-Mode. Docker-Builds müssen immer verifiziert werden. + +### 3. Agent Instructions +- **Codebase Investigator:** Nutze dieses Tool für Architektur-Fragen. +- **Testing:** Führe `npm run test` nach UI-Änderungen aus. Achte auf JSDOM-Einschränkungen (Mocking von `window.matchMedia` und `IntersectionObserver`). +- **CMS First:** Texte sollten nach Möglichkeit aus der `messages` Collection in Directus kommen, nicht hartcodiert werden. + +## Current State +- **Branch:** `dev` (pushed) +- **Status:** Design Overhaul abgeschlossen, Build stabil, Docker verifiziert. diff --git a/SAFE_PUSH_TO_MAIN.md b/SAFE_PUSH_TO_MAIN.md deleted file mode 100644 index e3e9162..0000000 --- a/SAFE_PUSH_TO_MAIN.md +++ /dev/null @@ -1,324 +0,0 @@ -# 🚀 Safe Push to Main Branch Guide - -**IMPORTANT**: This guide ensures you don't break production when merging to main. - -## ⚠️ Pre-Flight Checklist - -Before even thinking about pushing to main, verify ALL of these: - -### 1. Code Quality ✅ -```bash -# Run all checks -npm run build # Must pass with 0 errors -npm run lint # Must pass with 0 errors -npx tsc --noEmit # TypeScript must be clean -npx prisma format # Database schema must be valid -``` - -### 1b. Automated Testing ✅ -```bash -# Run comprehensive test suite (RECOMMENDED) -npm run test:all # Runs all tests including E2E - -# Or run individually: -npm run test # Unit tests -npm run test:critical # Critical path E2E tests -npm run test:hydration # Hydration tests -npm run test:email # Email API tests -``` - -### 2. Testing ✅ -```bash -# Automated testing (RECOMMENDED) -npm run test:all # Runs all automated tests - -# Manual testing (if needed) -npm run dev -# Test these critical paths: -# - Home page loads -# - Projects page works -# - Admin dashboard accessible -# - API endpoints respond -# - No console errors -# - No hydration errors -``` - -### 3. Database Changes ✅ -```bash -# If you changed the database schema: -# 1. Create migration -npx prisma migrate dev --name your_migration_name - -# 2. Test migration on a copy of production data -# 3. Document migration steps -# 4. Create rollback plan -``` - -### 4. Environment Variables ✅ -- [ ] All new env vars documented in `env.example` -- [ ] No secrets committed to git -- [ ] Production env vars are set on server -- [ ] Optional features have fallbacks - -### 5. Breaking Changes ✅ -- [ ] Documented in CHANGELOG -- [ ] Backward compatible OR migration plan exists -- [ ] Team notified of changes - ---- - -## 📋 Step-by-Step Push Process - -### Step 1: Ensure You're on Dev Branch -```bash -git checkout dev -git pull origin dev # Get latest changes -``` - -### Step 2: Final Verification -```bash -# Clean build -rm -rf .next node_modules/.cache -npm install -npm run build - -# Should complete without errors -``` - -### Step 3: Review Your Changes -```bash -# See what you're about to push -git log origin/main..dev --oneline -git diff origin/main..dev - -# Review carefully: -# - No accidental secrets -# - No debug code -# - No temporary files -# - All changes are intentional -``` - -### Step 4: Create a Backup Branch (Safety Net) -```bash -# Create backup before merging -git checkout -b backup-before-main-merge-$(date +%Y%m%d) -git push origin backup-before-main-merge-$(date +%Y%m%d) -git checkout dev -``` - -### Step 5: Merge Dev into Main (Local) -```bash -# Switch to main -git checkout main -git pull origin main # Get latest main - -# Merge dev into main -git merge dev --no-ff -m "Merge dev into main: [describe changes]" - -# If conflicts occur: -# 1. Resolve conflicts carefully -# 2. Test after resolving -# 3. Don't force push if unsure -``` - -### Step 6: Test the Merged Code -```bash -# Build and test the merged code -npm run build -npm run dev - -# Test critical paths again -# - Home page -# - Projects -# - Admin -# - APIs -``` - -### Step 7: Push to Main (If Everything Looks Good) -```bash -# Push to remote main -git push origin main - -# If you need to force push (DANGEROUS - only if necessary): -# git push origin main --force-with-lease -``` - -### Step 8: Monitor Deployment -```bash -# Watch your deployment logs -# Check for errors -# Verify health endpoints -# Test production site -``` - ---- - -## 🛡️ Safety Strategies - -### Strategy 1: Feature Flags -If you're adding new features, use feature flags: -```typescript -// In your code -if (process.env.ENABLE_NEW_FEATURE === 'true') { - // New feature code -} -``` - -### Strategy 2: Gradual Rollout -- Deploy to staging first -- Test thoroughly -- Then deploy to production -- Monitor closely - -### Strategy 3: Database Migrations -```bash -# Always test migrations first -# 1. Backup production database -# 2. Test migration on copy -# 3. Create rollback script -# 4. Run migration during low-traffic period -``` - -### Strategy 4: Rollback Plan -Always have a rollback plan: -```bash -# If something breaks: -git revert HEAD -git push origin main - -# Or rollback to previous commit: -git reset --hard -git push origin main --force-with-lease -``` - ---- - -## 🚨 Red Flags - DON'T PUSH IF: - -- ❌ Build fails -- ❌ Tests fail -- ❌ Linter errors -- ❌ TypeScript errors -- ❌ Database migration not tested -- ❌ Breaking changes not documented -- ❌ Secrets in code -- ❌ Debug code left in -- ❌ Console.logs everywhere -- ❌ Untested features -- ❌ No rollback plan - ---- - -## ✅ Green Lights - SAFE TO PUSH IF: - -- ✅ All checks pass -- ✅ Tested locally -- ✅ Database migrations tested -- ✅ No breaking changes (or documented) -- ✅ Documentation updated -- ✅ Team notified -- ✅ Rollback plan exists -- ✅ Feature flags for new features -- ✅ Environment variables documented - ---- - -## 📝 Pre-Push Checklist Template - -Copy this and check each item: - -``` -[ ] npm run build passes -[ ] npm run lint passes -[ ] npx tsc --noEmit passes -[ ] npx prisma format passes -[ ] npm run test:all passes (automated tests) -[ ] OR manual testing: - [ ] Dev server starts without errors - [ ] Home page loads correctly - [ ] Projects page works - [ ] Admin dashboard accessible - [ ] API endpoints respond - [ ] No console errors - [ ] No hydration errors -[ ] Database migrations tested (if any) -[ ] Environment variables documented -[ ] No secrets in code -[ ] Breaking changes documented -[ ] CHANGELOG updated -[ ] Team notified (if needed) -[ ] Rollback plan exists -[ ] Backup branch created -[ ] Changes reviewed -``` - ---- - -## 🔄 Alternative: Pull Request Workflow - -If you want extra safety, use PR workflow: - -```bash -# 1. Push dev branch -git push origin dev - -# 2. Create Pull Request on Git platform -# - Review changes -# - Get approval -# - Run CI/CD checks - -# 3. Merge PR to main (platform handles it) -``` - ---- - -## 🆘 Emergency Rollback - -If production breaks after push: - -### Quick Rollback -```bash -# 1. Revert the merge commit -git revert -m 1 -git push origin main - -# 2. Or reset to previous state -git reset --hard -git push origin main --force-with-lease -``` - -### Database Rollback -```bash -# If you ran migrations, roll them back: -npx prisma migrate resolve --rolled-back - -# Or restore from backup -``` - ---- - -## 📞 Need Help? - -If unsure: -1. **Don't push** - better safe than sorry -2. Test more thoroughly -3. Ask for code review -4. Use staging environment first -5. Create a PR for review - ---- - -## 🎯 Best Practices - -1. **Always test locally first** -2. **Use feature flags for new features** -3. **Test database migrations on copies** -4. **Document everything** -5. **Have a rollback plan** -6. **Monitor after deployment** -7. **Deploy during low-traffic periods** -8. **Keep main branch stable** - ---- - -**Remember**: It's better to delay a push than to break production! 🛡️ diff --git a/SECURITY_IMPROVEMENTS.md b/SECURITY_IMPROVEMENTS.md deleted file mode 100644 index 769de4a..0000000 --- a/SECURITY_IMPROVEMENTS.md +++ /dev/null @@ -1,120 +0,0 @@ -# 🔒 Security Improvements - -## Implemented Security Features - -### 1. n8n API Endpoint Protection - -All n8n endpoints are now protected with: -- **Authentication**: Admin authentication required for sensitive endpoints (`/api/n8n/generate-image`) -- **Rate Limiting**: - - `/api/n8n/generate-image`: 10 requests/minute - - `/api/n8n/chat`: 20 requests/minute - - `/api/n8n/status`: 30 requests/minute - -### 2. Email Obfuscation - -Email addresses can now be obfuscated to prevent automated scraping: - -```typescript -import { createObfuscatedMailto } from '@/lib/email-obfuscate'; -import { ObfuscatedEmail } from '@/components/ObfuscatedEmail'; - -// React component -Contact Me - -// HTML string -const mailtoLink = createObfuscatedMailto('contact@dk0.dev', 'Email Me'); -``` - -**How it works:** -- Emails are base64 encoded in the HTML -- JavaScript decodes them on click -- Prevents simple regex-based email scrapers -- Still functional for real users - -### 3. URL Obfuscation - -Sensitive URLs can be obfuscated: - -```typescript -import { createObfuscatedLink } from '@/lib/email-obfuscate'; - -const link = createObfuscatedLink('https://sensitive-url.com', 'Click Here'); -``` - -### 4. Rate Limiting - -All API endpoints have rate limiting: -- Prevents brute force attacks -- Protects against DDoS -- Configurable per endpoint - -## Code Obfuscation - -**Note**: Full code obfuscation for Next.js is **not recommended** because: - -1. **Next.js already minifies code** in production builds -2. **Obfuscation breaks source maps** (harder to debug) -3. **Performance impact** (slower execution) -4. **Not effective** - determined attackers can still reverse engineer -5. **Maintenance burden** - harder to debug issues - -**Better alternatives:** -- ✅ Minification (already enabled in Next.js) -- ✅ Environment variables for secrets -- ✅ Server-side rendering (code not exposed) -- ✅ API authentication -- ✅ Rate limiting -- ✅ Security headers - -## Best Practices - -### For Email Protection: -1. Use obfuscated emails in public HTML -2. Use contact forms instead of direct mailto links -3. Monitor for spam patterns - -### For API Protection: -1. Always require authentication for sensitive endpoints -2. Use rate limiting -3. Log suspicious activity -4. Use HTTPS only -5. Validate all inputs - -### For Webhook Protection: -1. Use secret tokens (`N8N_SECRET_TOKEN`) -2. Verify webhook signatures -3. Rate limit webhook endpoints -4. Monitor webhook usage - -## Implementation Status - -- ✅ n8n endpoints protected with auth + rate limiting -- ✅ Email obfuscation utility created -- ✅ URL obfuscation utility created -- ✅ Rate limiting on all n8n endpoints -- ⚠️ Email obfuscation not yet applied to pages (manual step) -- ⚠️ Code obfuscation not implemented (not recommended) - -## Next Steps - -To apply email obfuscation to your pages: - -1. Import the utility: -```typescript -import { ObfuscatedEmail } from '@/lib/email-obfuscate'; -``` - -2. Replace email links: -```tsx -// Before -Contact - -// After -Contact -``` - -3. For static HTML, use the string function: -```typescript -const html = createObfuscatedMailto('contact@dk0.dev', 'Email Me'); -``` diff --git a/SESSION_SUMMARY.md b/SESSION_SUMMARY.md new file mode 100644 index 0000000..5e6e2c6 --- /dev/null +++ b/SESSION_SUMMARY.md @@ -0,0 +1,42 @@ +# Session Summary - February 16, 2026 + +## 🛡️ Security & Technical Fixes +- **CSP Improvements:** Added `images.unsplash.com`, `*.dk0.dev`, and `localhost` to `img-src` and `connect-src`. +- **Worker Support:** Enabled `worker-src 'self' blob:;` for dynamic features. +- **Source Map Suppression:** Configured Webpack to ignore 404 errors for `framer-motion` and `LayoutGroupContext` source maps in development. +- **Project Filtering:** Unified the projects API to use Directus as the "Single Source of Truth," strictly enforcing the `published` status. + +## 🎨 UI/UX Enhancements (Liquid Editorial Bento) +- **Hero Section:** + - Stabilized the hero photo (removed floating animation). + - Fixed edge-clipping by increasing the border/padding. + - Removed redundant social buttons for a cleaner entry. +- **Activity Feed:** + - Full localization (DE/EN). + - Added a rotating cycle of CS-related quotes (Dijkstra, etc.) including CMS quotes. + - Redesigned Music UI with Spotify-themed branding (`#1DB954`), improved contrast, and animated frequency bars. +- **Contact Area:** + - Redesigned into a unified "Connect" Bento box. + - High-typography list style for Email, GitHub, LinkedIn, and Location. +- **Hobbies:** + - Added personalized descriptions reflecting interests like Analog Photography, Astronomy, and Traveling. + - Switched to a 4-column layout for better spatial balance. + +## 🚀 New Features +- **Snippets System ("The Lab"):** + - New Directus collection and API endpoint for technical notes. + - Interactive Bento-modals with code syntax highlighting and copy-to-clipboard functionality. + - Dedicated `/snippets` overview page. + - Implemented "Featured" logic to control visibility on the home page. +- **Redesigned 404 Page:** + - Completely rebuilt in the Editorial Bento style with clear navigation paths. +- **Visual Finish:** + - Added a subtle, animated CSS-based Grain/Noise overlay. + - Implemented smooth Page Transitions using Framer Motion. + +## 💻 Hardware Setup ("My Gear") +- Added a dedicated Bento card showing current dev setup: + - MacBook Pro M4 Pro (24GB RAM). + - PC: Ryzen 7 3800XT / RTX 3080. + - Server: IONOS Cloud & Raspberry Pi 4. + - Dual MSI 164Hz Curved Monitors. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6e45b00 --- /dev/null +++ b/TODO.md @@ -0,0 +1,28 @@ +# Portfolio Roadmap + +## Completed ✅ + +- [x] **Dark Mode Support**: `next-themes` integration, `ThemeToggle` component, and dark mode styles. +- [x] **Performance**: Replaced `` with Next.js `` for optimization. +- [x] **SEO**: Added JSON-LD Structured Data for projects. +- [x] **Security**: Rate limiting added to `book-reviews`, `hobbies`, and `tech-stack` APIs. +- [x] **Book Reviews**: + - `ReadBooks` component updated to handle optional ratings/reviews. + - `CurrentlyReading` component verified. + - Automation guide created (`docs/N8N_HARDCOVER_GUIDE.md`). +- [x] **Testing**: Added tests for `book-reviews`, `hobbies`, `tech-stack`, `CurrentlyReading`, and `ThemeToggle`. + +## Next Steps + +### Directus CMS +- [ ] **Messages Collection**: Create `messages` collection in Directus for dynamic i18n (currently using `messages/*.json`). +- [ ] **Projects Migration**: Finish migrating projects content to Directus (script exists: `scripts/migrate-projects-to-directus.js`). +- [ ] **Webhooks**: Configure Directus webhooks for On-Demand ISR Revalidation. + +### Features +- [ ] **Blog/Articles**: Design and implement the blog section. +- [ ] **Project Detail Gallery**: Add a lightbox/gallery for project screenshots. + +### DevOps +- [ ] **GitHub Actions**: Migrate CI/CD fully to GitHub Actions (from Gitea). +- [ ] **Docker Optimization**: Further reduce image size. diff --git a/app/[locale]/books/page.tsx b/app/[locale]/books/page.tsx new file mode 100644 index 0000000..0fc2fe6 --- /dev/null +++ b/app/[locale]/books/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Star, ArrowLeft } from "lucide-react"; +import Link from "next/link"; +import Image from "next/image"; +import { useEffect, useState } from "react"; +import { useLocale } from "next-intl"; +import { Skeleton } from "@/app/components/ui/Skeleton"; +import { BookReview } from "@/lib/directus"; + +export default function BooksPage() { + const locale = useLocale(); + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchBooks = async () => { + try { + const res = await fetch(`/api/book-reviews?locale=${locale}`); + const data = await res.json(); + if (data.bookReviews) setReviews(data.bookReviews); + } catch (error) { + console.error("Books fetch failed:", error); + } finally { + setLoading(false); + } + }; + fetchBooks(); + }, [locale]); + + return ( +
+
+
+ + + {locale === 'de' ? 'Zurück' : 'Back Home'} + +

+ Library. +

+

+ {locale === "de" + ? "Bücher, die meine Denkweise verändert und mein Wissen erweitert haben." + : "Books that shaped my mindset and expanded my horizons."} +

+
+ +
+ {loading ? ( + Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ + +
+
+ )) + ) : ( + reviews?.map((review) => ( +
+ {review.book_image && ( +
+ {review.book_title} +
+ )} +
+
+

{review.book_title}

+ {review.rating && ( +
+ + {review.rating} +
+ )} +
+

{review.book_author}

+ {review.review && ( +
+

+ “{review.review.replace(/<[^>]*>/g, '')}” +

+
+ )} +
+
+ )) + )} +
+
+
+ ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index c11214f..733b674 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -3,7 +3,6 @@ import { setRequestLocale } from "next-intl/server"; import React from "react"; import { notFound } from "next/navigation"; import ConsentBanner from "../components/ConsentBanner"; -import { getLocalizedMessage } from "@/lib/i18n-loader"; // Supported locales - must match middleware.ts const SUPPORTED_LOCALES = ["en", "de"] as const; diff --git a/app/[locale]/projects/[slug]/page.tsx b/app/[locale]/projects/[slug]/page.tsx index 9311494..6416294 100644 --- a/app/[locale]/projects/[slug]/page.tsx +++ b/app/[locale]/projects/[slug]/page.tsx @@ -3,6 +3,8 @@ import ProjectDetailClient from "@/app/_ui/ProjectDetailClient"; import { notFound } from "next/navigation"; import type { Metadata } from "next"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; +import { getProjectBySlug } from "@/lib/directus"; +import { ProjectDetailData } from "@/app/_ui/ProjectDetailClient"; export const revalidate = 300; @@ -12,6 +14,20 @@ export async function generateMetadata({ params: Promise<{ locale: string; slug: string }>; }): Promise { const { locale, slug } = await params; + + // Try Directus first for metadata + const directusProject = await getProjectBySlug(slug, locale); + if (directusProject) { + return { + title: directusProject.title, + description: directusProject.description, + alternates: { + canonical: toAbsoluteUrl(`/${locale}/projects/${slug}`), + languages: getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` }), + }, + }; + } + const languages = getLanguageAlternates({ pathWithoutLocale: `projects/${slug}` }); return { alternates: { @@ -28,7 +44,8 @@ export default async function ProjectPage({ }) { const { locale, slug } = await params; - const project = await prisma.project.findFirst({ + // Try PostgreSQL first + const dbProject = await prisma.project.findFirst({ where: { slug, published: true }, include: { translations: { @@ -37,29 +54,66 @@ export default async function ProjectPage({ }, }); - if (!project) return notFound(); + let projectData: ProjectDetailData | null = null; - const trPreferred = project.translations?.find((t) => t.locale === locale && (t?.title || t?.description)); - const trDefault = project.translations?.find( - (t) => t.locale === project.defaultLocale && (t?.title || t?.description), - ); - const tr = trPreferred ?? trDefault; - const { translations: _translations, ...rest } = project; - const localizedContent = (() => { - if (typeof tr?.content === "string") return tr.content; - if (tr?.content && typeof tr.content === "object" && "markdown" in tr.content) { - const markdown = (tr.content as Record).markdown; - if (typeof markdown === "string") return markdown; + if (dbProject) { + const trPreferred = dbProject.translations?.find((t) => t.locale === locale && (t?.title || t?.description)); + const trDefault = dbProject.translations?.find( + (t) => t.locale === dbProject.defaultLocale && (t?.title || t?.description), + ); + const tr = trPreferred ?? trDefault; + const { translations: _translations, ...rest } = dbProject; + const localizedContent = (() => { + if (typeof tr?.content === "string") return tr.content; + if (tr?.content && typeof tr.content === "object" && "markdown" in tr.content) { + const markdown = (tr.content as Record).markdown; + if (typeof markdown === "string") return markdown; + } + return dbProject.content; + })(); + projectData = { + ...rest, + title: tr?.title ?? dbProject.title, + description: tr?.description ?? dbProject.description, + content: localizedContent, + } as ProjectDetailData; + } else { + // Try Directus fallback + const directusProject = await getProjectBySlug(slug, locale); + if (directusProject) { + projectData = { + ...directusProject, + id: typeof directusProject.id === 'string' ? (parseInt(directusProject.id) || 0) : directusProject.id, + } as ProjectDetailData; } - return project.content; - })(); - const localized = { - ...rest, - title: tr?.title ?? project.title, - description: tr?.description ?? project.description, - content: localizedContent, + } + + if (!projectData) return notFound(); + + const jsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareSourceCode", + "name": projectData.title, + "description": projectData.description, + "codeRepository": projectData.github_url || projectData.github, + "programmingLanguage": projectData.technologies, + "author": { + "@type": "Person", + "name": "Dennis Konkol" + }, + "dateCreated": projectData.date || projectData.created_at, + "url": toAbsoluteUrl(`/${locale}/projects/${slug}`), + "image": (projectData.imageUrl || projectData.image_url) ? toAbsoluteUrl((projectData.imageUrl || projectData.image_url)!) : undefined, }; - return ; + return ( + <> + - - diff --git a/scripts/atomic-setup-book-reviews.js b/scripts/atomic-setup-book-reviews.js new file mode 100644 index 0000000..22def2b --- /dev/null +++ b/scripts/atomic-setup-book-reviews.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'POST', body = null) { + const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : null + }); + return res.ok ? await res.json() : { error: true, status: res.status }; +} + +async function atomicSetup() { + console.log('🚀 Starte atomares Setup...'); + + // 1. Die Haupt-Collection mit allen Feldern in EINEM Request + const setup = await api('collections', 'POST', { + collection: 'book_reviews', + schema: {}, + meta: { icon: 'import_contacts', display_template: '{{book_title}}' }, + fields: [ + { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true } }, + { field: 'status', type: 'string', schema: { default_value: 'draft' }, meta: { interface: 'select-dropdown' } }, + { field: 'book_title', type: 'string', schema: {}, meta: { interface: 'input' } }, + { field: 'book_author', type: 'string', schema: {}, meta: { interface: 'input' } }, + { field: 'book_image', type: 'string', schema: {}, meta: { interface: 'input' } }, + { field: 'rating', type: 'integer', schema: {}, meta: { interface: 'rating' } }, + { field: 'hardcover_id', type: 'string', schema: { is_unique: true }, meta: { interface: 'input' } }, + { field: 'finished_at', type: 'date', schema: {}, meta: { interface: 'datetime' } } + ] + }); + + if (setup.error) { console.error('Fehler bei Haupt-Collection:', setup); return; } + console.log('✅ Haupt-Collection steht.'); + + // 2. Die Übersetzungs-Collection + await api('collections', 'POST', { + collection: 'book_reviews_translations', + schema: {}, + meta: { hidden: true }, + fields: [ + { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true } }, + { field: 'book_reviews_id', type: 'integer', schema: {} }, + { field: 'languages_code', type: 'string', schema: {} }, + { field: 'review', type: 'text', schema: {}, meta: { interface: 'input-rich-text-html' } } + ] + }); + console.log('✅ Übersetzungstabelle steht.'); + + // 3. Die Relationen (Der Kleber) + await api('relations', 'POST', { collection: 'book_reviews_translations', field: 'book_reviews_id', related_collection: 'book_reviews', meta: { one_field: 'translations' }, schema: { on_delete: 'CASCADE' } }); + await api('relations', 'POST', { collection: 'book_reviews_translations', field: 'languages_code', related_collection: 'languages', schema: { on_delete: 'SET NULL' } }); + + // 4. Das Translations-Feld in der Haupt-Collection registrieren + await api('fields/book_reviews', 'POST', { + field: 'translations', + type: 'alias', + meta: { interface: 'translations', special: ['translations'], options: { languageField: 'languages_code' } } + }); + + console.log('✨ Alles fertig! Bitte lade Directus neu.'); +} + +atomicSetup().catch(console.error); diff --git a/scripts/cleanup-directus-ui.js b/scripts/cleanup-directus-ui.js new file mode 100644 index 0000000..dc71024 --- /dev/null +++ b/scripts/cleanup-directus-ui.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json(); + return { ok: response.ok, data }; +} + +async function finalCleanup() { + console.log('🧹 Räume Directus UI auf...'); + + // 1. Haupt-Collection konfigurieren + await api('collections/book_reviews', 'PATCH', { + meta: { + icon: 'import_contacts', + display_template: '{{book_title}}', + hidden: false, + group: null, // Aus Ordnern herausholen + singleton: false, + translations: [ + { language: 'de-DE', translation: 'Buch-Bewertungen' }, + { language: 'en-US', translation: 'Book Reviews' } + ] + } + }); + + // 2. Übersetzungs-Tabelle verstecken (Wichtig für die Optik!) + await api('collections/book_reviews_translations', 'PATCH', { + meta: { + hidden: true, + group: 'book_reviews' // Technisch untergeordnet + } + }); + + // 3. Sicherstellen, dass das 'translations' Feld im CMS gut aussieht + await api('fields/book_reviews/translations', 'PATCH', { + meta: { + interface: 'translations', + display: 'translations', + options: { + languageField: 'languages_code', + userLanguage: true + } + } + }); + + console.log('✅ UI optimiert! Bitte lade Directus jetzt neu (Cmd+R / Strg+R).'); + console.log('Du solltest jetzt links in der Navigation "Book Reviews" mit einem Buch-Icon sehen.'); +} + +finalCleanup().catch(console.error); diff --git a/scripts/deep-fix-languages.js b/scripts/deep-fix-languages.js new file mode 100644 index 0000000..b7cccbb --- /dev/null +++ b/scripts/deep-fix-languages.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; +} + +async function deepFixLanguages() { + console.log('🏗 Starte Deep-Fix...'); + const collections = ['projects', 'book_reviews', 'hobbies', 'tech_stack_categories', 'messages']; + for (const coll of collections) { + const transColl = coll + '_translations'; + console.log('🛠 Fixe ' + transColl); + await api('relations/' + transColl + '/languages_code', 'DELETE').catch(() => {}); + await api('relations', 'POST', { + collection: transColl, + field: 'languages_code', + related_collection: 'languages', + schema: {}, + meta: { interface: 'select-dropdown', options: { template: '{{name}}' } } + }); + await api('fields/' + transColl + '/languages_code', 'PATCH', { + meta: { interface: 'select-dropdown', display: 'raw', required: true } + }); + } + console.log('✅ Fertig! Bitte lade Directus neu.'); +} +deepFixLanguages().catch(console.error); diff --git a/scripts/emergency-directus-fix.js b/scripts/emergency-directus-fix.js new file mode 100644 index 0000000..fc75701 --- /dev/null +++ b/scripts/emergency-directus-fix.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; +} + +async function emergencyFix() { + console.log('Fixing...'); + await api('collections/book_reviews', 'PATCH', { meta: { hidden: true } }); + await api('collections/book_reviews', 'PATCH', { meta: { hidden: false, icon: 'book' } }); + await api('fields/book_reviews/translations', 'PATCH', { + meta: { interface: 'translations', options: { languageField: 'languages_code' } } + }); + console.log('Done. Please reload in Incognito.'); +} +emergencyFix().catch(console.error); diff --git a/scripts/final-directus-ui-fix.js b/scripts/final-directus-ui-fix.js new file mode 100644 index 0000000..e527eae --- /dev/null +++ b/scripts/final-directus-ui-fix.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'PATCH', body = null) { + const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : null + }); + return res.ok ? await res.json() : { error: true }; +} + +async function finalDirectusUiFix() { + console.log('🛠 Finaler UI-Fix für Status und Übersetzungen...'); + + // 1. Status-Dropdown Optionen hinzufügen + await api('fields/book_reviews/status', 'PATCH', { + meta: { + interface: 'select-dropdown', + options: { + choices: [ + { text: 'Draft', value: 'draft' }, + { text: 'Published', value: 'published' } + ] + } + } + }); + + // 2. Translations Interface auf Tabs (translations) umstellen + await api('fields/book_reviews/translations', 'PATCH', { + meta: { + interface: 'translations', + special: ['translations'], + options: { + languageField: 'languages_code' + } + } + }); + + console.log('✅ UI-Einstellungen korrigiert! Bitte lade Directus neu.'); +} + +finalDirectusUiFix().catch(console.error); diff --git a/scripts/fix-auto-id.js b/scripts/fix-auto-id.js new file mode 100644 index 0000000..99198c4 --- /dev/null +++ b/scripts/fix-auto-id.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'PATCH', body = null) { + const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : null + }); + const data = await res.json().catch(() => ({})); + return { ok: res.ok, data }; +} + +async function fixAutoId() { + console.log('🛠 Automatisierung der IDs für Übersetzungen...'); + + // 1. ID Feld in der Übersetzungstabelle konfigurieren + await api('fields/book_reviews_translations/id', 'PATCH', { + meta: { + hidden: true, + interface: 'input', + readonly: true, + special: null + } + }); + + // 2. Fremdschlüssel (book_reviews_id) verstecken, da Directus das intern regelt + await api('fields/book_reviews_translations/book_reviews_id', 'PATCH', { + meta: { + hidden: true, + interface: 'input', + readonly: true + } + }); + + // 3. Sprach-Code Feld konfigurieren + await api('fields/book_reviews_translations/languages_code', 'PATCH', { + meta: { + interface: 'select-dropdown', + width: 'half', + options: { + choices: [ + { text: 'Deutsch', value: 'de-DE' }, + { text: 'English', value: 'en-US' } + ] + } + } + }); + + console.log('✅ Fertig! Die IDs werden nun automatisch im Hintergrund verwaltet.'); +} + +fixAutoId().catch(console.error); diff --git a/scripts/fix-book-reviews-ui.js b/scripts/fix-book-reviews-ui.js new file mode 100644 index 0000000..720372f --- /dev/null +++ b/scripts/fix-book-reviews-ui.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + return response.ok ? await response.json() : { error: true }; +} + +async function fixUI() { + console.log('🔧 Repariere Directus UI für Book Reviews...'); + + // 1. Status Feld verschönern + await api('fields/book_reviews/status', 'PATCH', { + meta: { + interface: 'select-dropdown', + display: 'labels', + display_options: { + showAsDot: true, + choices: [ + { value: 'published', foreground: '#FFFFFF', background: '#00C897' }, + { value: 'draft', foreground: '#FFFFFF', background: '#666666' } + ] + }, + options: { + choices: [ + { text: 'Veröffentlicht', value: 'published' }, + { text: 'Entwurf', value: 'draft' } + ] + } + } + }); + + // 2. Sprachen-Verknüpfung reparieren (WICHTIG für Tabs) + await api('relations', 'POST', { + collection: 'book_reviews_translations', + field: 'languages_code', + related_collection: 'languages', + meta: { interface: 'select-dropdown' } + }).catch(() => console.log('Relation existiert evtl. schon...')); + + // 3. Übersetzungs-Interface aktivieren + await api('fields/book_reviews/translations', 'PATCH', { + meta: { + interface: 'translations', + display: 'translations', + options: { + languageField: 'languages_code', + userLanguage: true + } + } + }); + + console.log('✅ UI-Fix angewendet! Bitte lade Directus neu.'); +} + +fixUI().catch(console.error); diff --git a/scripts/fix-directus-book-reviews.js b/scripts/fix-directus-book-reviews.js new file mode 100644 index 0000000..2869736 --- /dev/null +++ b/scripts/fix-directus-book-reviews.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json(); + return { ok: response.ok, data, status: response.status }; +} + +async function fix() { + console.log('🔧 Fixing Directus Book Reviews...'); + await api('collections/book_reviews', 'PATCH', { meta: { icon: 'menu_book', display_template: '{{book_title}}', hidden: false } }); + + // Link to system languages + await api('relations', 'POST', { + collection: 'book_reviews_translations', + field: 'languages_code', + related_collection: 'languages', + meta: { interface: 'select-dropdown' } + }); + + // UI Improvements + await api('fields/book_reviews/status', 'PATCH', { meta: { interface: 'select-dropdown', display: 'labels' } }); + await api('fields/book_reviews/rating', 'PATCH', { meta: { interface: 'rating', display: 'rating' } }); + await api('fields/book_reviews_translations/review', 'PATCH', { meta: { interface: 'input-rich-text-html' } }); + + console.log('✅ Fix applied! Bitte lade Directus neu und setze die Permissions auf Public.'); +} +fix().catch(console.error); diff --git a/scripts/fix-messages-collection.js b/scripts/fix-messages-collection.js new file mode 100644 index 0000000..8d686bf --- /dev/null +++ b/scripts/fix-messages-collection.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'POST', body = null) { + const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : null + }); + const data = await res.json().catch(() => ({})); + return { ok: res.ok, data }; +} + +async function fixMessagesCollection() { + console.log('🛠 Repariere "messages" Collection...'); + + // 1. Key-Feld hinzufügen (falls es fehlt) + // Wir nutzen type: string und schema: {} um eine echte Spalte zu erzeugen + const fieldRes = await api('fields/messages', 'POST', { + field: 'key', + type: 'string', + schema: { + is_nullable: false, + is_unique: true + }, + meta: { + interface: 'input', + options: { placeholder: 'z.B. hero.title' }, + required: true + } + }); + + if (fieldRes.ok) { + console.log('✅ "key" Feld erfolgreich erstellt.'); + } else { + console.log('⚠️ "key" Feld konnte nicht erstellt werden (existiert evtl schon).'); + } + + // 2. Übersetzungs-Feld in der Untertabelle reparieren + console.log('🛠 Prüfe messages_translations...'); + await api('fields/messages_translations', 'POST', { + field: 'value', + type: 'text', + schema: {}, + meta: { interface: 'input-multiline' } + }).catch(() => {}); + + console.log('✅ Fix abgeschlossen! Bitte lade Directus neu.'); +} + +fixMessagesCollection().catch(console.error); diff --git a/scripts/fix-relations-metadata.js b/scripts/fix-relations-metadata.js new file mode 100644 index 0000000..fd8d96b --- /dev/null +++ b/scripts/fix-relations-metadata.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; +} + +async function fixRelationsMetadata() { + console.log('🔗 Fixe Sprach-Relationen Metadaten...'); + + const collections = ['projects', 'book_reviews', 'hobbies', 'tech_stack_categories', 'messages']; + + for (const coll of collections) { + const transColl = `${coll}_translations`; + console.log(`🛠 Konfiguriere ${transColl}...`); + + // Wir müssen die Relation von languages_code zur languages Tabelle + // für Directus "greifbar" machen. + await api(`relations/${transColl}/languages_code`, 'PATCH', { + meta: { + interface: 'select-dropdown', + display: 'raw' + } + }); + + // WICHTIG: Wir sagen dem Hauptfeld "translations" noch einmal + // ganz explizit, welches Feld in der Untertabelle für die Sprache zuständig ist. + await api(`fields/${coll}/translations`, 'PATCH', { + meta: { + interface: 'translations', + options: { + languageField: 'languages_code' // Der Name des Feldes in der *_translations Tabelle + } + } + }); + } + + console.log('✅ Fertig! Bitte lade Directus neu.'); +} + +fixRelationsMetadata().catch(console.error); diff --git a/scripts/fix-translation-interface.js b/scripts/fix-translation-interface.js new file mode 100644 index 0000000..2bcc579 --- /dev/null +++ b/scripts/fix-translation-interface.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; +} + +async function fixTranslationInterface() { + console.log('🛠 Erdenke das Translations-Interface neu...'); + + const collections = ['projects', 'book_reviews', 'hobbies', 'tech_stack_categories', 'messages']; + + for (const coll of collections) { + console.log(`🔧 Fixe Interface für ${coll}...`); + + // Wir überschreiben die Metadaten des Feldes "translations" + // WICHTIG: Wir setzen interface auf 'translations' und mappen das languageField + await api(`fields/${coll}/translations`, 'PATCH', { + meta: { + interface: 'translations', + display: 'translations', + special: ['translations'], + options: { + languageField: 'languages_code', + userLanguage: true, + defaultLanguage: 'de-DE' + } + } + }); + + // Wir stellen sicher, dass in der Untertabelle das Feld languages_code + // als 'languages' Typ erkannt wird + await api(`fields/${coll}_translations/languages_code`, 'PATCH', { + meta: { + interface: 'select-dropdown', + special: null // Kein spezielles Feld hier, nur ein normaler FK + } + }); + } + + console.log('✅ Übersetzung-Tabs sollten jetzt erscheinen! Bitte Directus hart neu laden.'); +} + +fixTranslationInterface().catch(console.error); diff --git a/scripts/force-translations-plus.js b/scripts/force-translations-plus.js new file mode 100644 index 0000000..459fe70 --- /dev/null +++ b/scripts/force-translations-plus.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; +} + +async function forceTranslationsPlusButton() { + console.log('🔨 Erzwinge "Plus"-Button für Übersetzungen...'); + + const coll = 'book_reviews'; + const transColl = 'book_reviews_translations'; + + // 1. Das alte Alias-Feld löschen (falls es klemmt) + await api(`fields/${coll}/translations`, 'DELETE').catch(() => {}); + + // 2. Das Feld komplett neu anlegen als technisches Alias für die Relation + await api(`fields/${coll}`, 'POST', { + field: 'translations', + type: 'alias', + meta: { + interface: 'translations', + display: 'translations', + special: ['translations'], + options: { + languageField: 'languages_code', + userLanguage: true + }, + width: 'full' + } + }); + + // 3. Die Relation explizit als One-to-Many (O2M) registrieren + // Das ist der wichtigste Schritt für den Plus-Button! + await api('relations', 'POST', { + collection: transColl, + field: 'book_reviews_id', + related_collection: coll, + meta: { + one_field: 'translations', + junction_field: null, + one_deselect_action: 'delete' + }, + schema: { + on_delete: 'CASCADE' + } + }).catch(err => console.log('Relation existiert evtl. schon, überspringe...')); + + console.log('✅ Fertig! Bitte lade Directus neu.'); + console.log('Gehe in ein Buch -> Jetzt MUSS unten bei "Translations" ein Plus-Button oder "Create New" stehen.'); +} + +forceTranslationsPlusButton().catch(console.error); diff --git a/scripts/global-cms-beauty-fix.js b/scripts/global-cms-beauty-fix.js new file mode 100644 index 0000000..f6419d7 --- /dev/null +++ b/scripts/global-cms-beauty-fix.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json().catch(() => ({})); + return { ok: response.ok, data }; +} + +async function globalBeautyFix() { + console.log('✨ Starte globale CMS-Verschönerung...'); + await api('items/languages', 'POST', { code: 'de-DE', name: 'German' }).catch(() => {}); + await api('items/languages', 'POST', { code: 'en-US', name: 'English' }).catch(() => {}); + + const collections = ['projects', 'book_reviews', 'hobbies', 'tech_stack_categories', 'messages']; + for (const coll of collections) { + console.log('📦 Optimiere ' + coll); + await api('collections/' + coll + '_translations', 'PATCH', { meta: { hidden: true } }); + await api('fields/' + coll + '/translations', 'PATCH', { + meta: { + interface: 'translations', + display: 'translations', + width: 'full', + options: { languageField: 'languages_code', defaultLanguage: 'de-DE', userLanguage: true } + } + }); + await api('relations', 'POST', { + collection: coll + '_translations', + field: 'languages_code', + related_collection: 'languages', + meta: { interface: 'select-dropdown' } + }).catch(() => {}); + } + await api('fields/projects/tags', 'PATCH', { meta: { interface: 'tags' } }); + await api('fields/projects/technologies', 'PATCH', { meta: { interface: 'tags' } }); + console.log('✅ CMS ist jetzt aufgeräumt! Bitte Directus neu laden.'); +} +globalBeautyFix().catch(console.error); diff --git a/scripts/make-directus-editable.js b/scripts/make-directus-editable.js new file mode 100644 index 0000000..43746b4 --- /dev/null +++ b/scripts/make-directus-editable.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json(); + return { ok: response.ok, data }; +} + +async function makeEditable() { + console.log('🔓 Mache "Book Reviews" editierbar...'); + + // 1. Prüfen ob ID Feld existiert, sonst anlegen + console.log('1. Erstelle ID-Primärschlüssel...'); + await api('fields/book_reviews', 'POST', { + field: 'id', + type: 'integer', + schema: { + is_primary_key: true, + has_auto_increment: true + }, + meta: { + hidden: true // Im Formular verstecken, da automatisch + } + }); + + // 2. Sicherstellen, dass alle Felder eine Interface-Zuweisung haben (wichtig für die Eingabe) + console.log('2. Konfiguriere Eingabe-Interfaces...'); + const fieldUpdates = [ + { field: 'book_title', interface: 'input' }, + { field: 'book_author', interface: 'input' }, + { field: 'rating', interface: 'rating' }, + { field: 'status', interface: 'select-dropdown' }, + { field: 'finished_at', interface: 'datetime' } + ]; + + for (const f of fieldUpdates) { + await api(`fields/book_reviews/${f.field}`, 'PATCH', { + meta: { interface: f.interface, readonly: false } + }); + } + + console.log('✅ Fertig! Bitte lade Directus neu.'); + console.log('Solltest du immer noch nicht editieren können, musst du eventuell die Collection löschen und neu anlegen lassen, da die Datenbank-Struktur (ID) manchmal nicht nachträglich über die API geändert werden kann.'); +} + +makeEditable().catch(console.error); diff --git a/scripts/master-setup-book-reviews.js b/scripts/master-setup-book-reviews.js new file mode 100644 index 0000000..49e8b73 --- /dev/null +++ b/scripts/master-setup-book-reviews.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json(); + return { ok: response.ok, data }; +} + +async function masterSetup() { + console.log('🚀 Starte Master-Setup...'); + await api('collections', 'POST', { collection: 'book_reviews', meta: { icon: 'import_contacts', display_template: '{{book_title}}' } }); + await api('fields/book_reviews', 'POST', { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true }, meta: { hidden: true } }); + + const fields = [ + { field: 'status', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [{text: 'Published', value: 'published'}, {text: 'Draft', value: 'draft'}] } }, schema: { default_value: 'draft' } }, + { field: 'book_title', type: 'string', meta: { interface: 'input' } }, + { field: 'book_author', type: 'string', meta: { interface: 'input' } }, + { field: 'book_image', type: 'string', meta: { interface: 'input' } }, + { field: 'rating', type: 'integer', meta: { interface: 'rating' } }, + { field: 'hardcover_id', type: 'string', meta: { interface: 'input' }, schema: { is_unique: true } }, + { field: 'finished_at', type: 'date', meta: { interface: 'datetime' } } + ]; + for (const f of fields) await api('fields/book_reviews', 'POST', f); + + await api('collections', 'POST', { collection: 'book_reviews_translations', meta: { hidden: true } }); + await api('fields/book_reviews_translations', 'POST', { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true }, meta: { hidden: true } }); + await api('fields/book_reviews_translations', 'POST', { field: 'book_reviews_id', type: 'integer' }); + await api('fields/book_reviews_translations', 'POST', { field: 'languages_code', type: 'string' }); + await api('fields/book_reviews_translations', 'POST', { field: 'review', type: 'text', meta: { interface: 'input-rich-text-html' } }); + + await api('relations', 'POST', { collection: 'book_reviews_translations', field: 'book_reviews_id', related_collection: 'book_reviews', meta: { one_field: 'translations' } }); + await api('relations', 'POST', { collection: 'book_reviews_translations', field: 'languages_code', related_collection: 'languages' }); + await api('fields/book_reviews', 'POST', { field: 'translations', type: 'alias', meta: { interface: 'translations', special: ['translations'] } }); + + console.log('✨ Setup abgeschlossen! Bitte lade Directus neu und setze die Public-Permissions.'); +} +masterSetup().catch(console.error); diff --git a/scripts/migrate-content-pages-to-directus.js b/scripts/migrate-content-pages-to-directus.js deleted file mode 100644 index 55de48e..0000000 --- a/scripts/migrate-content-pages-to-directus.js +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env node -/** - * Migrate Content Pages from PostgreSQL (Prisma) to Directus - * - * - Copies `content_pages` + translations from Postgres into Directus - * - Creates or updates items per (slug, locale) - * - * Usage: - * DATABASE_URL=postgresql://... DIRECTUS_STATIC_TOKEN=... DIRECTUS_URL=... \ - * node scripts/migrate-content-pages-to-directus.js - */ - -const fetch = require('node-fetch'); -const { PrismaClient } = require('@prisma/client'); -require('dotenv').config(); - -const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; -const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; - -if (!DIRECTUS_TOKEN) { - console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in env'); - process.exit(1); -} - -const prisma = new PrismaClient(); - -const localeMap = { - en: 'en-US', - de: 'de-DE', -}; - -function toDirectusLocale(locale) { - return localeMap[locale] || locale; -} - -async function directusRequest(endpoint, method = 'GET', body = null) { - const url = `${DIRECTUS_URL}/${endpoint}`; - const options = { - method, - headers: { - Authorization: `Bearer ${DIRECTUS_TOKEN}`, - 'Content-Type': 'application/json', - }, - }; - - if (body) { - options.body = JSON.stringify(body); - } - - const res = await fetch(url, options); - if (!res.ok) { - const text = await res.text(); - throw new Error(`HTTP ${res.status} on ${endpoint}: ${text}`); - } - return res.json(); -} - -async function upsertContentIntoDirectus({ slug, locale, status, title, content }) { - const directusLocale = toDirectusLocale(locale); - - // allow locale-specific slug variants: base for en, base-locale for others - const slugVariant = directusLocale === 'en-US' ? slug : `${slug}-${directusLocale.toLowerCase()}`; - - const payload = { - slug: slugVariant, - locale: directusLocale, - status: status?.toLowerCase?.() === 'published' ? 'published' : status || 'draft', - title: title || slug, - content: content || null, - }; - - try { - const { data } = await directusRequest('items/content_pages', 'POST', payload); - console.log(` ➕ Created ${slugVariant} (${directusLocale}) [id=${data?.id}]`); - return data?.id; - } catch (error) { - const msg = error?.message || ''; - if (msg.includes('already exists') || msg.includes('duplicate key') || msg.includes('UNIQUE')) { - console.log(` ⚠️ Skipping ${slugVariant} (${directusLocale}) – already exists`); - return null; - } - throw error; - } -} - -async function migrateContentPages() { - console.log('\n📦 Migrating Content Pages from PostgreSQL to Directus...'); - - const pages = await prisma.contentPage.findMany({ - include: { translations: true }, - }); - - console.log(`Found ${pages.length} pages in PostgreSQL`); - - for (const page of pages) { - const status = page.status || 'PUBLISHED'; - for (const tr of page.translations) { - await upsertContentIntoDirectus({ - slug: page.key, - locale: tr.locale, - status, - title: tr.title, - content: tr.content, - }); - } - } - - console.log('✅ Content page migration finished.'); -} - -async function main() { - try { - await prisma.$connect(); - await migrateContentPages(); - } catch (error) { - console.error('❌ Migration failed:', error.message); - process.exit(1); - } finally { - await prisma.$disconnect(); - } -} - -main(); diff --git a/scripts/migrate-hobbies-to-directus.js b/scripts/migrate-hobbies-to-directus.js deleted file mode 100644 index 0548fbf..0000000 --- a/scripts/migrate-hobbies-to-directus.js +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env node -/** - * Migrate Hobbies to Directus - * - * Migriert Hobbies-Daten aus messages/en.json und messages/de.json nach Directus - * - * Usage: - * node scripts/migrate-hobbies-to-directus.js - */ - -const fetch = require('node-fetch'); -const fs = require('fs'); -const path = require('path'); -require('dotenv').config(); - -const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; -const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; - -if (!DIRECTUS_TOKEN) { - console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env'); - process.exit(1); -} - -const messagesEn = JSON.parse( - fs.readFileSync(path.join(__dirname, '../messages/en.json'), 'utf-8') -); -const messagesDe = JSON.parse( - fs.readFileSync(path.join(__dirname, '../messages/de.json'), 'utf-8') -); - -const hobbiesEn = messagesEn.home.about.hobbies; -const hobbiesDe = messagesDe.home.about.hobbies; - -const HOBBIES_DATA = [ - { - key: 'self_hosting', - icon: 'Code', - titleEn: hobbiesEn.selfHosting, - titleDe: hobbiesDe.selfHosting - }, - { - key: 'gaming', - icon: 'Gamepad2', - titleEn: hobbiesEn.gaming, - titleDe: hobbiesDe.gaming - }, - { - key: 'game_servers', - icon: 'Server', - titleEn: hobbiesEn.gameServers, - titleDe: hobbiesDe.gameServers - }, - { - key: 'jogging', - icon: 'Activity', - titleEn: hobbiesEn.jogging, - titleDe: hobbiesDe.jogging - } -]; - -async function directusRequest(endpoint, method = 'GET', body = null) { - const url = `${DIRECTUS_URL}/${endpoint}`; - const options = { - method, - headers: { - 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, - 'Content-Type': 'application/json' - } - }; - - if (body) { - options.body = JSON.stringify(body); - } - - try { - const response = await fetch(url, options); - if (!response.ok) { - const text = await response.text(); - throw new Error(`HTTP ${response.status}: ${text}`); - } - return await response.json(); - } catch (error) { - console.error(`Error calling ${method} ${endpoint}:`, error.message); - throw error; - } -} - -async function migrateHobbies() { - console.log('\n📦 Migrating Hobbies to Directus...\n'); - - for (const hobby of HOBBIES_DATA) { - console.log(`\n🎮 Hobby: ${hobby.key}`); - - try { - // 1. Create Hobby - console.log(' Creating hobby...'); - const hobbyData = { - key: hobby.key, - icon: hobby.icon, - status: 'published', - sort: HOBBIES_DATA.indexOf(hobby) + 1 - }; - - const { data: createdHobby } = await directusRequest( - 'items/hobbies', - 'POST', - hobbyData - ); - - console.log(` ✅ Hobby created with ID: ${createdHobby.id}`); - - // 2. Create Translations - console.log(' Creating translations...'); - - // English Translation - await directusRequest( - 'items/hobbies_translations', - 'POST', - { - hobbies_id: createdHobby.id, - languages_code: 'en-US', - title: hobby.titleEn - } - ); - - // German Translation - await directusRequest( - 'items/hobbies_translations', - 'POST', - { - hobbies_id: createdHobby.id, - languages_code: 'de-DE', - title: hobby.titleDe - } - ); - - console.log(' ✅ Translations created (en-US, de-DE)'); - - } catch (error) { - console.error(` ❌ Error migrating ${hobby.key}:`, error.message); - } - } - - console.log('\n✨ Migration complete!\n'); -} - -async function verifyMigration() { - console.log('\n🔍 Verifying Migration...\n'); - - try { - const { data: hobbies } = await directusRequest( - 'items/hobbies?fields=key,icon,status,translations.title,translations.languages_code' - ); - - console.log(`✅ Found ${hobbies.length} hobbies in Directus:`); - hobbies.forEach(h => { - const enTitle = h.translations?.find(t => t.languages_code === 'en-US')?.title; - console.log(` - ${h.key}: "${enTitle}"`); - }); - - console.log('\n🎉 Hobbies successfully migrated!\n'); - console.log('Next steps:'); - console.log(' 1. Visit: https://cms.dk0.dev/admin/content/hobbies'); - console.log(' 2. Update About.tsx to load hobbies from Directus\n'); - - } catch (error) { - console.error('❌ Verification failed:', error.message); - } -} - -async function main() { - console.log('\n╔════════════════════════════════════════╗'); - console.log('║ Hobbies Migration to Directus ║'); - console.log('╚════════════════════════════════════════╝\n'); - - try { - await migrateHobbies(); - await verifyMigration(); - } catch (error) { - console.error('\n❌ Migration failed:', error); - process.exit(1); - } -} - -main(); diff --git a/scripts/migrate-tech-stack-to-directus.js b/scripts/migrate-tech-stack-to-directus.js deleted file mode 100644 index edc77f9..0000000 --- a/scripts/migrate-tech-stack-to-directus.js +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env node -/** - * Directus Tech Stack Migration Script - * - * Migriert bestehende Tech Stack Daten aus messages/en.json und messages/de.json - * nach Directus Collections. - * - * Usage: - * npm install node-fetch@2 dotenv - * node scripts/migrate-tech-stack-to-directus.js - */ - -const fetch = require('node-fetch'); -const fs = require('fs'); -const path = require('path'); -require('dotenv').config(); - -const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; -const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; - -if (!DIRECTUS_TOKEN) { - console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env'); - process.exit(1); -} - -// Lade aktuelle Tech Stack Daten aus messages files -const messagesEn = JSON.parse( - fs.readFileSync(path.join(__dirname, '../messages/en.json'), 'utf-8') -); -const messagesDe = JSON.parse( - fs.readFileSync(path.join(__dirname, '../messages/de.json'), 'utf-8') -); - -const techStackEn = messagesEn.home.about.techStack; -const techStackDe = messagesDe.home.about.techStack; - -// Tech Stack Struktur aus About.tsx -const TECH_STACK_DATA = [ - { - key: 'frontend', - icon: 'Globe', - nameEn: techStackEn.categories.frontendMobile, - nameDe: techStackDe.categories.frontendMobile, - items: ['Next.js', 'Tailwind CSS', 'Flutter'] - }, - { - key: 'backend', - icon: 'Server', - nameEn: techStackEn.categories.backendDevops, - nameDe: techStackDe.categories.backendDevops, - items: ['Docker', 'PostgreSQL', 'Redis', 'Traefik'] - }, - { - key: 'tools', - icon: 'Wrench', - nameEn: techStackEn.categories.toolsAutomation, - nameDe: techStackDe.categories.toolsAutomation, - items: ['Git', 'CI/CD', 'n8n', techStackEn.items.selfHostedServices] - }, - { - key: 'security', - icon: 'Shield', - nameEn: techStackEn.categories.securityAdmin, - nameDe: techStackDe.categories.securityAdmin, - items: ['CrowdSec', 'Suricata', 'Proxmox'] - } -]; - -async function directusRequest(endpoint, method = 'GET', body = null) { - const url = `${DIRECTUS_URL}/${endpoint}`; - const options = { - method, - headers: { - 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, - 'Content-Type': 'application/json' - } - }; - - if (body) { - options.body = JSON.stringify(body); - } - - try { - const response = await fetch(url, options); - if (!response.ok) { - const text = await response.text(); - throw new Error(`HTTP ${response.status}: ${text}`); - } - return await response.json(); - } catch (error) { - console.error(`Error calling ${method} ${endpoint}:`, error.message); - throw error; - } -} - -async function ensureLanguagesExist() { - console.log('\n🌍 Checking Languages...'); - - try { - const { data: languages } = await directusRequest('items/languages'); - const hasEnUS = languages.some(l => l.code === 'en-US'); - const hasDeDE = languages.some(l => l.code === 'de-DE'); - - if (!hasEnUS) { - console.log(' Creating en-US language...'); - await directusRequest('items/languages', 'POST', { - code: 'en-US', - name: 'English (United States)' - }); - } - - if (!hasDeDE) { - console.log(' Creating de-DE language...'); - await directusRequest('items/languages', 'POST', { - code: 'de-DE', - name: 'German (Germany)' - }); - } - - console.log(' ✅ Languages ready'); - } catch (error) { - console.log(' ⚠️ Languages collection might not exist yet'); - } -} - -async function migrateTechStack() { - console.log('\n📦 Migrating Tech Stack to Directus...\n'); - - await ensureLanguagesExist(); - - for (const category of TECH_STACK_DATA) { - console.log(`\n📁 Category: ${category.key}`); - - try { - // 1. Create Category - console.log(' Creating category...'); - const categoryData = { - key: category.key, - icon: category.icon, - status: 'published', - sort: TECH_STACK_DATA.indexOf(category) + 1 - }; - - const { data: createdCategory } = await directusRequest( - 'items/tech_stack_categories', - 'POST', - categoryData - ); - - console.log(` ✅ Category created with ID: ${createdCategory.id}`); - - // 2. Create Translations - console.log(' Creating translations...'); - - // English Translation - await directusRequest( - 'items/tech_stack_categories_translations', - 'POST', - { - tech_stack_categories_id: createdCategory.id, - languages_code: 'en-US', - name: category.nameEn - } - ); - - // German Translation - await directusRequest( - 'items/tech_stack_categories_translations', - 'POST', - { - tech_stack_categories_id: createdCategory.id, - languages_code: 'de-DE', - name: category.nameDe - } - ); - - console.log(' ✅ Translations created (en-US, de-DE)'); - - // 3. Create Items - console.log(` Creating ${category.items.length} items...`); - - for (let i = 0; i < category.items.length; i++) { - const itemName = category.items[i]; - await directusRequest( - 'items/tech_stack_items', - 'POST', - { - category: createdCategory.id, - name: itemName, - sort: i + 1 - } - ); - console.log(` ✅ ${itemName}`); - } - - } catch (error) { - console.error(` ❌ Error migrating ${category.key}:`, error.message); - } - } - - console.log('\n✨ Migration complete!\n'); -} - -async function verifyMigration() { - console.log('\n🔍 Verifying Migration...\n'); - - try { - const { data: categories } = await directusRequest( - 'items/tech_stack_categories?fields=*,translations.*,items.*' - ); - - console.log(`✅ Found ${categories.length} categories:`); - categories.forEach(cat => { - const enTranslation = cat.translations?.find(t => t.languages_code === 'en-US'); - const itemCount = cat.items?.length || 0; - console.log(` - ${cat.key}: "${enTranslation?.name}" (${itemCount} items)`); - }); - - console.log('\n🎉 All data migrated successfully!\n'); - console.log('Next steps:'); - console.log(' 1. Visit https://cms.dk0.dev/admin/content/tech_stack_categories'); - console.log(' 2. Verify data looks correct'); - console.log(' 3. Run: npm run dev:directus (to test GraphQL queries)'); - console.log(' 4. Update About.tsx to use Directus data\n'); - - } catch (error) { - console.error('❌ Verification failed:', error.message); - } -} - -// Main execution -(async () => { - try { - await migrateTechStack(); - await verifyMigration(); - } catch (error) { - console.error('\n❌ Migration failed:', error); - process.exit(1); - } -})(); diff --git a/scripts/perfect-directus-structure.js b/scripts/perfect-directus-structure.js new file mode 100644 index 0000000..f73fb6a --- /dev/null +++ b/scripts/perfect-directus-structure.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json(); + return { ok: response.ok, data }; +} + +async function perfectStructure() { + console.log('💎 Optimiere CMS-Struktur zur Perfektion...'); + + // 1. Projekt-Buttons individualisieren + console.log('1. Erweitere Projekte um individuelle Button-Labels...'); + await api('fields/projects_translations', 'POST', { field: 'button_live_label', type: 'string', meta: { interface: 'input', options: { placeholder: 'z.B. Live Demo, App öffnen...' }, width: 'half' } }); + await api('fields/projects_translations', 'POST', { field: 'button_github_label', type: 'string', meta: { interface: 'input', options: { placeholder: 'z.B. Source Code, GitHub...' }, width: 'half' } }); + + // 2. SEO für Inhaltsseiten + console.log('2. Füge SEO-Felder zu Content-Pages hinzu...'); + await api('fields/content_pages', 'POST', { field: 'meta_description', type: 'string', meta: { interface: 'input' } }); + await api('fields/content_pages', 'POST', { field: 'keywords', type: 'string', meta: { interface: 'input' } }); + + // 3. Die ultimative "Messages" Collection (für UI Strings) + console.log('3. Erstelle globale "Messages" Collection...'); + await api('collections', 'POST', { collection: 'messages', schema: {}, meta: { icon: 'translate', display_template: '{{key}}' } }); + await api('fields/messages', 'POST', { field: 'key', type: 'string', schema: { is_primary_key: true }, meta: { interface: 'input', options: { placeholder: 'home.hero.title' } } }); + + // Messages Translations + await api('collections', 'POST', { collection: 'messages_translations', schema: {}, meta: { hidden: true } }); + await api('fields/messages_translations', 'POST', { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true }, meta: { hidden: true } }); + await api('fields/messages_translations', 'POST', { field: 'messages_id', type: 'string', schema: {} }); + await api('fields/messages_translations', 'POST', { field: 'languages_code', type: 'string', schema: {} }); + await api('fields/messages_translations', 'POST', { field: 'value', type: 'text', meta: { interface: 'input' } }); + + // Relationen für Messages + await api('relations', 'POST', { collection: 'messages_translations', field: 'messages_id', related_collection: 'messages', meta: { one_field: 'translations' } }); + await api('relations', 'POST', { collection: 'messages_translations', field: 'languages_code', related_collection: 'languages' }); + await api('fields/messages', 'POST', { field: 'translations', type: 'alias', meta: { interface: 'translations', special: ['translations'] } }); + + console.log('✨ CMS Struktur ist jetzt perfekt! Lade Directus neu.'); +} + +perfectStructure().catch(console.error); diff --git a/scripts/seed-cms-content.js b/scripts/seed-cms-content.js new file mode 100644 index 0000000..25c0f26 --- /dev/null +++ b/scripts/seed-cms-content.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'POST', body = null) { + const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : null + }); + return res.ok ? await res.json() : { error: true, status: res.status }; +} + +const seedData = [ + { key: 'hero.badge', de: 'Student & Self-Hoster', en: 'Student & Self-Hoster' }, + { key: 'hero.line1', de: 'Building', en: 'Building' }, + { key: 'hero.line2', de: 'Stuff.', en: 'Stuff.' }, + { key: 'about.quote.idle', de: 'Gerade am Planen des nächsten großen Projekts.', en: 'Currently planning the next big thing.' } +]; + +async function seedMessages() { + console.log('🌱 Befülle Directus mit Inhalten...'); + for (const item of seedData) { + console.log(`- Erstelle Key: ${item.key}`); + const res = await api('items/messages', 'POST', { + key: item.key, + translations: [ + { languages_code: 'de-DE', value: item.de }, + { languages_code: 'en-US', value: item.en } + ] + }); + } + console.log('✅ CMS erfolgreich befüllt!'); +} + +seedMessages().catch(console.error); diff --git a/scripts/set-public-permissions.js b/scripts/set-public-permissions.js new file mode 100644 index 0000000..84db63a --- /dev/null +++ b/scripts/set-public-permissions.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function setPublicPermissions() { + console.log('🔓 Setze Public-Berechtigungen für Book Reviews...'); + + // Wir holen die ID der Public Rolle + const rolesRes = await fetch(`${DIRECTUS_URL}/roles`, { headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}` } }); + const roles = await rolesRes.json(); + const publicRole = roles.data.find(r => r.name.toLowerCase() === 'public'); + + if (!publicRole) return console.error('Public Rolle nicht gefunden.'); + + const collections = ['book_reviews', 'book_reviews_translations']; + + for (const coll of collections) { + console.log(`- Erlaube Lesezugriff auf ${coll}`); + await fetch(`${DIRECTUS_URL}/permissions`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + role: publicRole.id, + collection: coll, + action: 'read', + permissions: {}, + validation: null, + fields: ['*'] + }) + }); + } + + console.log('✅ Fertig! Die Website sollte die Daten jetzt lesen können.'); +} + +setPublicPermissions().catch(console.error); diff --git a/scripts/setup-directus-book-reviews.js b/scripts/setup-directus-book-reviews.js new file mode 100644 index 0000000..d3fab37 --- /dev/null +++ b/scripts/setup-directus-book-reviews.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +/** + * Setup Book Reviews Collection in Directus + */ + +require('dotenv').config(); + +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +if (!DIRECTUS_TOKEN) { + console.error('❌ Error: DIRECTUS_STATIC_TOKEN not found in .env'); + process.exit(1); +} + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { + 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, + 'Content-Type': 'application/json' + } + }; + if (body) options.body = JSON.stringify(body); + + try { + const response = await fetch(url, options); + const data = await response.json(); + if (!response.ok) { + if (data.errors?.[0]?.extensions?.code === 'RECORD_NOT_UNIQUE' || data.errors?.[0]?.message?.includes('already exists')) { + return { alreadyExists: true }; + } + return { error: true, message: data.errors?.[0]?.message }; + } + return data; + } catch (error) { + return { error: true, message: error.message }; + } +} + +async function setup() { + console.log('🚀 Starting Directus Book Reviews Setup...'); + + const coll = await api('collections', 'POST', { + collection: 'book_reviews', + meta: { icon: 'menu_book', display_template: '{{book_title}}' } + }); + console.log(coll.alreadyExists ? ' ⚠️ Collection exists.' : ' ✅ Collection created.'); + + const fields = [ + { field: 'status', type: 'string', meta: { interface: 'select-dropdown' } }, + { field: 'book_title', type: 'string', meta: { interface: 'input' } }, + { field: 'book_author', type: 'string', meta: { interface: 'input' } }, + { field: 'book_image', type: 'string', meta: { interface: 'input' } }, + { field: 'rating', type: 'integer', meta: { interface: 'slider' } }, + { field: 'hardcover_id', type: 'string', meta: { interface: 'input' } }, + { field: 'finished_at', type: 'date', meta: { interface: 'datetime' } } + ]; + + for (const f of fields) { + await api('fields/book_reviews', 'POST', f); + } + console.log(' ✅ Fields created.'); + + await api('collections', 'POST', { collection: 'book_reviews_translations', meta: { hidden: true } }); + await api('fields/book_reviews_translations', 'POST', { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true } }); + await api('fields/book_reviews_translations', 'POST', { field: 'book_reviews_id', type: 'integer' }); + await api('fields/book_reviews_translations', 'POST', { field: 'languages_code', type: 'string' }); + await api('fields/book_reviews_translations', 'POST', { field: 'review', type: 'text', meta: { interface: 'input-multiline' } }); + + await api('fields/book_reviews', 'POST', { field: 'translations', type: 'alias', meta: { interface: 'translations', special: ['translations'] } }); + await api('relations', 'POST', { + collection: 'book_reviews_translations', + field: 'book_reviews_id', + related_collection: 'book_reviews', + meta: { one_field: 'translations' } + }); + + console.log('\n✨ Setup Complete!'); +} + +setup().catch(console.error); diff --git a/scripts/setup-snippets.js b/scripts/setup-snippets.js new file mode 100644 index 0000000..7a64791 --- /dev/null +++ b/scripts/setup-snippets.js @@ -0,0 +1,79 @@ + +/* eslint-disable @typescript-eslint/no-require-imports */ +const fetch = require('node-fetch'); +require('dotenv').config(); + +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function setupSnippets() { + console.log('📦 Setting up Snippets collection...'); + + // 1. Create Collection + try { + await fetch(`${DIRECTUS_URL}/collections`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + collection: 'snippets', + meta: { icon: 'terminal', display_template: '{{title}}' }, + schema: { name: 'snippets' } + }) + }); + } catch (_e) {} + + // 2. Add Fields + const fields = [ + { field: 'status', type: 'string', meta: { interface: 'select-dropdown' }, schema: { default_value: 'published' } }, + { field: 'title', type: 'string', meta: { interface: 'input' } }, + { field: 'category', type: 'string', meta: { interface: 'input' } }, + { field: 'code', type: 'text', meta: { interface: 'input-code' } }, + { field: 'description', type: 'text', meta: { interface: 'textarea' } }, + { field: 'language', type: 'string', meta: { interface: 'input' }, schema: { default_value: 'javascript' } }, + { field: 'featured', type: 'boolean', meta: { interface: 'boolean' }, schema: { default_value: false } } + ]; + + for (const f of fields) { + try { + await fetch(`${DIRECTUS_URL}/fields/snippets`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(f) + }); + } catch (_e) {} + } + + // 3. Add Example Data + const exampleSnippets = [ + { + title: 'Traefik SSL Config', + category: 'Docker', + language: 'yaml', + featured: true, + description: "Meine Standard-Konfiguration für automatisches SSL via Let's Encrypt in Docker Swarm.", + code: "labels:\n - \"traefik.enable=true\"\n - \"traefik.http.routers.myapp.rule=Host(`example.com`)\"\n - \"traefik.http.routers.myapp.entrypoints=websecure\"\n - \"traefik.http.routers.myapp.tls.certresolver=myresolver\"" + }, + { + title: 'Docker Cleanup Alias', + category: 'ZSH', + language: 'bash', + featured: true, + description: 'Ein einfacher Alias, um ungenutzte Docker-Container, Images und Volumes schnell zu entfernen.', + code: "alias dclean='docker system prune -af --volumes'" + } + ]; + + for (const s of exampleSnippets) { + try { + await fetch(`${DIRECTUS_URL}/items/snippets`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(s) + }); + } catch (_e) {} + } + + console.log('✅ Snippets setup complete!'); +} + +setupSnippets(); diff --git a/scripts/simple-translation-fix.js b/scripts/simple-translation-fix.js new file mode 100644 index 0000000..c298312 --- /dev/null +++ b/scripts/simple-translation-fix.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'PATCH', body = null) { + const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : null + }); + return res.ok ? await res.json() : { error: true }; +} + +async function simpleFix() { + console.log('🛠 Vereinfache Übersetzungs-Interface...'); + + // 1. Das Hauptfeld "translations" konfigurieren + await api('fields/book_reviews/translations', 'PATCH', { + meta: { + interface: 'translations', + options: { + languageField: 'languages_code', + userLanguage: false, // Deaktivieren, um Fehler zu vermeiden + defaultLanguage: null + } + } + }); + + // 2. Das Sprachfeld in der Untertabelle konfigurieren + await api('fields/book_reviews_translations/languages_code', 'PATCH', { + meta: { + interface: 'select-dropdown', + options: { + choices: [ + { text: 'Deutsch', value: 'de-DE' }, + { text: 'English', value: 'en-US' } + ] + } + } + }); + + console.log('✅ Fertig! Bitte lade Directus neu.'); +} + +simpleFix().catch(console.error); diff --git a/scripts/ultra-setup-book-reviews.js b/scripts/ultra-setup-book-reviews.js new file mode 100644 index 0000000..969d7fd --- /dev/null +++ b/scripts/ultra-setup-book-reviews.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +require('dotenv').config(); +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function api(endpoint, method = 'GET', body = null) { + const url = `${DIRECTUS_URL}/${endpoint}`; + const options = { + method, + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' } + }; + if (body) options.body = JSON.stringify(body); + const response = await fetch(url, options); + const data = await response.json(); + return { ok: response.ok, data }; +} + +async function ultraSetup() { + console.log('🚀 Starte Ultra-Setup für "Book Reviews"...'); + + // 1. Collection anlegen (mit Primärschlüssel-Definition im Schema!) + await api('collections', 'POST', { + collection: 'book_reviews', + schema: {}, + meta: { icon: 'import_contacts', display_template: '{{book_title}}' } + }); + + // 2. Felder mit explizitem Schema (Datenbank-Spalten) + const fields = [ + { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true }, meta: { hidden: true } }, + { field: 'status', type: 'string', schema: { default_value: 'draft' }, meta: { interface: 'select-dropdown', width: 'half' } }, + { field: 'book_title', type: 'string', schema: {}, meta: { interface: 'input', width: 'full' } }, + { field: 'book_author', type: 'string', schema: {}, meta: { interface: 'input', width: 'half' } }, + { field: 'rating', type: 'integer', schema: {}, meta: { interface: 'rating', width: 'half' } }, + { field: 'book_image', type: 'string', schema: {}, meta: { interface: 'input', width: 'full' } }, + { field: 'hardcover_id', type: 'string', schema: { is_unique: true }, meta: { interface: 'input', width: 'half' } }, + { field: 'finished_at', type: 'date', schema: {}, meta: { interface: 'datetime', width: 'half' } } + ]; + + for (const f of fields) { + await api('fields/book_reviews', 'POST', f); + } + + // 3. Übersetzungen + await api('collections', 'POST', { collection: 'book_reviews_translations', schema: {}, meta: { hidden: true } }); + + const transFields = [ + { field: 'id', type: 'integer', schema: { is_primary_key: true, has_auto_increment: true }, meta: { hidden: true } }, + { field: 'book_reviews_id', type: 'integer', schema: {}, meta: { hidden: true } }, + { field: 'languages_code', type: 'string', schema: {}, meta: { interface: 'select-dropdown' } }, + { field: 'review', type: 'text', schema: {}, meta: { interface: 'input-rich-text-html' } } + ]; + + for (const f of transFields) { + await api('fields/book_reviews_translations', 'POST', f); + } + + // 4. Relationen (Der Kleber) + await api('relations', 'POST', { collection: 'book_reviews_translations', field: 'book_reviews_id', related_collection: 'book_reviews', meta: { one_field: 'translations' }, schema: { on_delete: 'CASCADE' } }); + await api('relations', 'POST', { collection: 'book_reviews_translations', field: 'languages_code', related_collection: 'languages', schema: { on_delete: 'SET NULL' } }); + await api('fields/book_reviews', 'POST', { field: 'translations', type: 'alias', meta: { interface: 'translations', special: ['translations'] } }); + + console.log('✅ Ultra-Setup abgeschlossen! Bitte lade Directus neu.'); +} + +ultraSetup().catch(console.error); diff --git a/scripts/update-hobbies.js b/scripts/update-hobbies.js new file mode 100644 index 0000000..a61c7f3 --- /dev/null +++ b/scripts/update-hobbies.js @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const fetch = require('node-fetch'); +require('dotenv').config(); + +const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev'; +const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN; + +async function syncHobbies() { + const hobbies = [ + { + key: 'gym', + icon: 'Activity', + translations: [ + { languages_code: 'de-DE', title: 'Gym', description: 'Bin wieder regelmäßig im Training.' }, + { languages_code: 'en-US', title: 'Gym', description: 'Back at training regularly.' } + ] + }, + { + key: 'skiing', + icon: 'Activity', + translations: [ + { languages_code: 'de-DE', title: 'Skifahren', description: 'Ich liebe es, auf der Piste zu sein.' }, + { languages_code: 'en-US', title: 'Skiing', description: 'Love being out on the slopes.' } + ] + }, + { + key: 'programming', + icon: 'Code', + translations: [ + { languages_code: 'de-DE', title: 'Programmieren', description: 'Mache ich einfach gerne, auch privat.' }, + { languages_code: 'en-US', title: 'Programming', description: 'Just love building things, even in my free time.' } + ] + }, + { + key: 'reading', + icon: 'BookOpen', + translations: [ + { languages_code: 'de-DE', title: 'Lesen', description: 'Lese einfach gerne zur Entspannung.' }, + { languages_code: 'en-US', title: 'Reading', description: 'Enjoy reading to relax.' } + ] + }, + { + key: 'gaming', + icon: 'Gamepad2', + translations: [ + { languages_code: 'de-DE', title: 'Zocken', description: 'Ab und zu mal eine Runde zocken.' }, + { languages_code: 'en-US', title: 'Gaming', description: 'Playing some games every now and then.' } + ] + }, + { + key: 'series', + icon: 'Tv', + translations: [ + { languages_code: 'de-DE', title: 'Serien', description: 'Ich schaue gerne gute Serien.' }, + { languages_code: 'en-US', title: 'Series', description: 'I enjoy watching good series.' } + ] + }, + { + key: 'boardgames', + icon: 'Gamepad2', + translations: [ + { languages_code: 'de-DE', title: 'Gesellschaftsspiele', description: 'Mag Gesellschaftsspiele mit Freunden.' }, + { languages_code: 'en-US', title: 'Board Games', description: 'Love board games with friends.' } + ] + }, + { + key: 'traveling', + icon: 'Plane', + translations: [ + { languages_code: 'de-DE', title: 'Reisen', description: 'Ich reise einfach gerne.' }, + { languages_code: 'en-US', title: 'Traveling', description: 'I just love to travel.' } + ] + }, + { + key: 'analog_photography', + icon: 'Camera', + translations: [ + { languages_code: 'de-DE', title: 'Analoge Fotografie', description: 'Lese mich gerade in das Thema ein.' }, + { languages_code: 'en-US', title: 'Analog Photography', description: 'Currently reading into the topic.' } + ] + }, + { + key: 'astronomy', + icon: 'Stars', + translations: [ + { languages_code: 'de-DE', title: 'Astronomie', description: 'Fasziniert vom Universum und Sternen.' }, + { languages_code: 'en-US', title: 'Astronomy', description: 'Fascinated by the universe and stars.' } + ] + }, + { + key: 'guitar', + icon: 'Music', + translations: [ + { languages_code: 'de-DE', title: 'Gitarre', description: 'Spiele gelegentlich, wenn auch unregelmäßig.' }, + { languages_code: 'en-US', title: 'Guitar', description: 'Playing occasionally, even if not regularly.' } + ] + } + ]; + + console.log('🔄 Syncing hobbies to Directus...'); + + for (const hobby of hobbies) { + try { + const searchRes = await fetch(`${DIRECTUS_URL}/items/hobbies?filter[key][_eq]=${hobby.key}`, { + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}` } + }); + const searchData = await searchRes.json(); + + if (searchData.data && searchData.data.length > 0) { + const id = searchData.data[0].id; + console.log(`Updating ${hobby.key}...`); + await fetch(`${DIRECTUS_URL}/items/hobbies/${id}`, { + method: 'PATCH', + headers: { + 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + status: 'published', + icon: hobby.icon, + translations: hobby.translations + }) + }); + } else { + console.log(`Creating ${hobby.key}...`); + await fetch(`${DIRECTUS_URL}/items/hobbies`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + key: hobby.key, + status: 'published', + icon: hobby.icon, + translations: hobby.translations + }) + }); + } + } catch (_e) { + console.error(`Failed to sync ${hobby.key}:`, _e.message); + } + } + + // Delete 'gameServers' if it exists + try { + const delSearch = await fetch(`${DIRECTUS_URL}/items/hobbies?filter[key][_eq]=gameServers`, { + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}` } + }); + const delData = await delSearch.json(); + if (delData.data && delData.data.length > 0) { + console.log('Removing gameServers hobby...'); + await fetch(`${DIRECTUS_URL}/items/hobbies/${delData.data[0].id}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}` } + }); + } + } catch (_e) {} + + console.log('✅ Done!'); +} + +syncHobbies();