From 3ccb8bd5850321159efc0d24ee425618feaeb0e7 Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Thu, 8 Jan 2026 14:57:58 +0000 Subject: [PATCH] feat(BlogWoman): add Favorites, Series collections and content blocks Add new collections and blocks for BlogWoman affiliate and video content: Collections: - Favorites: Affiliate products with categories, badges, and price ranges - Series: YouTube series with custom branding (logo, colors) Blocks: - FavoritesBlock: Grid/list/carousel display for affiliate products - SeriesBlock: Series overview with filtering - SeriesDetailBlock: Single series page with hero - VideoEmbedBlock: YouTube/Vimeo embed with privacy mode - FeaturedContentBlock: Curated mixed-content collections Also includes documentation updates for deployment and API guides. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 147 +++++++++++-- docs/DEPLOYMENT.md | 94 ++++---- docs/DEPLOYMENT_STRATEGY.md | 4 +- docs/INFRASTRUCTURE.md | 12 +- docs/PROJECT_STATUS.md | 11 +- docs/STAGING-DEPLOYMENT.md | 12 +- docs/anleitungen/API_ANLEITUNG.md | 100 ++++----- docs/anleitungen/SECURITY.md | 23 +- docs/anleitungen/TODO.md | 20 +- src/blocks/FavoritesBlock.ts | 182 +++++++++++++++ src/blocks/FeaturedContentBlock.ts | 342 +++++++++++++++++++++++++++++ src/blocks/SeriesBlock.ts | 154 +++++++++++++ src/blocks/SeriesDetailBlock.ts | 155 +++++++++++++ src/blocks/VideoEmbedBlock.ts | 294 +++++++++++++++++++++++++ src/blocks/index.ts | 7 + src/collections/Favorites.ts | 214 ++++++++++++++++++ src/collections/Pages.ts | 12 + src/collections/Series.ts | 173 +++++++++++++++ src/payload.config.ts | 10 + 19 files changed, 1816 insertions(+), 150 deletions(-) create mode 100644 src/blocks/FavoritesBlock.ts create mode 100644 src/blocks/FeaturedContentBlock.ts create mode 100644 src/blocks/SeriesBlock.ts create mode 100644 src/blocks/SeriesDetailBlock.ts create mode 100644 src/blocks/VideoEmbedBlock.ts create mode 100644 src/collections/Favorites.ts create mode 100644 src/collections/Series.ts diff --git a/CLAUDE.md b/CLAUDE.md index a049b22..8a13378 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -704,12 +704,14 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10; \dt -- Alle Tabellen ``` -## Blocks Übersicht +## Blocks Übersicht (42 Blocks) +### Core Blocks | Block | Slug | Beschreibung | |-------|------|--------------| | HeroBlock | hero-block | Einzelner Hero mit Bild, Headline, CTA | | HeroSliderBlock | hero-slider-block | Hero-Slider mit mehreren Slides | +| ImageSliderBlock | image-slider-block | Bildergalerie/Karussell | | TextBlock | text-block | Textinhalt mit Rich-Text | | ImageTextBlock | image-text-block | Bild + Text nebeneinander | | CardGridBlock | card-grid-block | Karten-Raster | @@ -719,6 +721,10 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10; | TimelineBlock | timeline-block | Timeline-Darstellung | | DividerBlock | divider-block | Trennlinie | | VideoBlock | video-block | Video einbetten | + +### Content Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| | PostsListBlock | posts-list-block | Beitrags-Liste | | TestimonialsBlock | testimonials-block | Kundenbewertungen | | NewsletterBlock | newsletter-block | Newsletter-Anmeldung | @@ -726,8 +732,54 @@ SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10; | FAQBlock | faq-block | FAQ-Akkordeon | | TeamBlock | team-block | Team-Mitglieder | | ServicesBlock | services-block | Leistungen | + +### Blogging Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| +| AuthorBioBlock | author-bio-block | Autoren-Biografie | +| RelatedPostsBlock | related-posts-block | Verwandte Beiträge | +| ShareButtonsBlock | share-buttons-block | Social Share Buttons | +| TableOfContentsBlock | table-of-contents-block | Inhaltsverzeichnis | + +### Team Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| +| TeamFilterBlock | team-filter-block | Team mit Filter-Funktion | +| OrgChartBlock | org-chart-block | Organigramm | + +### Feature Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| +| LocationsBlock | locations-block | Standorte/Filialen | +| LogoGridBlock | logo-grid-block | Partner/Kunden-Logos | +| StatsBlock | stats-block | Statistiken/Zahlen | +| JobsBlock | jobs-block | Stellenangebote | +| DownloadsBlock | downloads-block | Download-Bereich | +| MapBlock | map-block | Karten-Einbindung | + +### Interactive Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| +| EventsBlock | events-block | Veranstaltungen | +| PricingBlock | pricing-block | Preistabellen | +| TabsBlock | tabs-block | Tab-Navigation | +| AccordionBlock | accordion-block | Akkordeon/Aufklappbar | +| ComparisonBlock | comparison-block | Vergleichstabelle | + +### Tenant-specific Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| | BeforeAfterBlock | before-after-block | Vorher/Nachher Bildvergleich (porwoll.de) | +### BlogWoman Blocks +| Block | Slug | Beschreibung | +|-------|------|--------------| +| FavoritesBlock | favorites-block | Affiliate-Produkte Grid/Liste/Karussell | +| SeriesBlock | series-block | YouTube-Serien Übersicht | +| SeriesDetailBlock | series-detail-block | Serien-Einzelseite mit Hero | +| VideoEmbedBlock | video-embed-block | YouTube/Vimeo Embed mit Privacy Mode | +| FeaturedContentBlock | featured-content-block | Kuratierte Mixed-Content Sammlung | + ### HeroSliderBlock Features Vollwertiger Hero-Slider mit: @@ -761,39 +813,95 @@ Vollwertiger Hero-Slider mit: - Separate Mobile-Höhe - Content-Breite -## Collections Übersicht +## Collections Übersicht (40+ Collections) +### Core Collections | Collection | Slug | Beschreibung | |------------|------|--------------| | Users | users | Benutzer mit isSuperAdmin Flag | | Tenants | tenants | Mandanten mit E-Mail-Konfiguration | | Media | media | Medien mit 11 responsive Image Sizes | | Pages | pages | Seiten mit Blocks | + +### Content Collections +| Collection | Slug | Beschreibung | +|------------|------|--------------| | Posts | posts | Blog/News/Presse mit Kategorien | | Categories | categories | Kategorien für Posts | -| Portfolios | portfolios | Portfolio-Galerien (Fotografie) | -| PortfolioCategories | portfolio-categories | Kategorien für Portfolios | +| Tags | tags | Tags für Posts (Blogging) | +| Authors | authors | Autoren für Posts | | Testimonials | testimonials | Kundenbewertungen | | FAQs | faqs | Häufig gestellte Fragen (FAQ) | +| SocialLinks | social-links | Social Media Links | + +### Team & Services +| Collection | Slug | Beschreibung | +|------------|------|--------------| | Team | team | Team-Mitglieder und Mitarbeiter | | ServiceCategories | service-categories | Kategorien für Leistungen | | Services | services | Leistungen und Dienstleistungen | -| NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In | -| SocialLinks | social-links | Social Media Links | -| Forms | forms | Formular-Builder | -| FormSubmissions | form-submissions | Formular-Einsendungen mit Status-Workflow | -| EmailLogs | email-logs | E-Mail-Protokollierung | -| AuditLogs | audit-logs | Security Audit Trail | -| CookieConfigurations | cookie-configurations | Cookie-Banner Konfiguration | -| CookieInventory | cookie-inventory | Cookie-Inventar | -| ConsentLogs | consent-logs | Consent-Protokollierung | +| Jobs | jobs | Stellenangebote | + +### Portfolio & Media +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| Portfolios | portfolios | Portfolio-Galerien (Fotografie) | +| PortfolioCategories | portfolio-categories | Kategorien für Portfolios | +| Videos | videos | Video-Bibliothek mit YouTube/Vimeo/Uploads | +| VideoCategories | video-categories | Kategorien für Videos | + +### Products (E-Commerce) +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| Products | products | Produkte | +| ProductCategories | product-categories | Produkt-Kategorien | + +### Feature Collections +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| Locations | locations | Standorte/Filialen | +| Partners | partners | Partner/Kunden | +| Downloads | downloads | Download-Dateien | +| Events | events | Veranstaltungen | | Timelines | timelines | Chronologische Events (Geschichte, Meilensteine) | | Workflows | workflows | Komplexe Prozesse mit Phasen und Schritten | + +### Formulare & Newsletter +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| Forms | forms | Formular-Builder (Plugin) | +| FormSubmissions | form-submissions | Formular-Einsendungen mit CRM-Workflow | +| NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In | + +### Consent & Privacy (DSGVO) +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| CookieConfigurations | cookie-configurations | Cookie-Banner Konfiguration | +| CookieInventory | cookie-inventory | Cookie-Inventar | +| ConsentLogs | consent-logs | Consent-Protokollierung (WORM) | +| PrivacyPolicySettings | privacy-policy-settings | Datenschutz-Einstellungen | + +### Tenant-specific Collections +| Collection | Slug | Beschreibung | +|------------|------|--------------| | Bookings | bookings | Fotografie-Buchungen (porwoll.de) | | Certifications | certifications | Zertifizierungen (C2S) | | Projects | projects | Game-Development-Projekte (gunshin.de) | -| Videos | videos | Video-Bibliothek mit YouTube/Vimeo/Uploads | -| VideoCategories | video-categories | Kategorien für Videos | + +### BlogWoman Collections +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| Favorites | favorites | Affiliate-Produkte mit Kategorien und Badges | +| Series | series | YouTube-Serien mit Branding (Logo, Farben) | + +### System Collections +| Collection | Slug | Beschreibung | +|------------|------|--------------| +| EmailLogs | email-logs | E-Mail-Protokollierung | +| AuditLogs | audit-logs | Security Audit Trail | +| SiteSettings | site-settings | Website-Einstellungen (pro Tenant) | +| Navigations | navigations | Navigationsmenüs (pro Tenant) | +| Redirects | redirects | URL-Weiterleitungen (Plugin) | ## Timeline Collection @@ -930,12 +1038,11 @@ Die FormSubmissions Collection wurde zu einem leichtgewichtigen CRM erweitert: ## Globals +> **Hinweis:** SiteSettings, Navigations und PrivacyPolicySettings wurden zu tenant-spezifischen Collections umgewandelt (siehe Collections Übersicht). + | Global | Slug | Beschreibung | |--------|------|--------------| -| SiteSettings | site-settings | Allgemeine Website-Einstellungen | -| Navigation | navigation | Navigationsmenü | -| SEOSettings | seo-settings | SEO-Einstellungen | -| PrivacyPolicySettings | privacy-policy-settings | Datenschutz-Einstellungen | +| SEOSettings | seo-settings | Globale SEO-Einstellungen (systemweit) | ## Test Suite @@ -1070,4 +1177,4 @@ ssh payload@162.55.85.18 ### Scripts & Backup - `scripts/backup/README.md` - Backup-System Dokumentation -*Letzte Aktualisierung: 27.12.2025* +*Letzte Aktualisierung: 29.12.2025* diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 03d8dbd..87552eb 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,6 +1,8 @@ # Deployment Guide - Payload CMS Multi-Tenant -*Letzte Aktualisierung: 18. Dezember 2025* +*Letzte Aktualisierung: 27. Dezember 2025* + +> **Wichtig:** Für die vollständige Deployment-Strategie siehe [DEPLOYMENT_STRATEGY.md](./DEPLOYMENT_STRATEGY.md) ## Übersicht @@ -94,61 +96,59 @@ pm2 logs payload --lines 20 ## Production Deployment (main → cms.c2sgmbh.de) -### Schritt 1: Merge zu main +### Option A: Via GitHub Actions (Empfohlen) -```bash -# Auf dem Development-Server oder lokal -git checkout main -git pull origin main -git merge develop -git push origin main -``` +1. **develop in main mergen und pushen** + ```bash + git checkout main + git pull origin main + git merge develop + git push origin main + ``` -### Schritt 2: Deploy auf Hetzner 3 +2. **GitHub Actions Workflow starten** + - Gehe zu: Actions → "Deploy to Production" → "Run workflow" + - Oder via CLI: + ```bash + gh workflow run deploy-production.yml + ``` + +3. **Der Workflow führt automatisch aus:** + - Pre-flight Checks + - Tests (optional) + - Datenbank-Backup + - Deployment + - Health Check + - Bei Fehler: Automatischer Rollback + +### Option B: Via Deploy-Script auf Server ```bash # SSH zum Production-Server ssh payload@162.55.85.18 -# Deploy-Script ausführen -~/deploy.sh +# Deploy-Script ausführen (mit Backup, Health Check, Rollback-Fähigkeit) +cd ~/payload-cms +./scripts/deploy-production.sh + +# Optionen: +./scripts/deploy-production.sh -y # Ohne Bestätigung +./scripts/deploy-production.sh --skip-backup # Ohne Backup (nicht empfohlen) +./scripts/deploy-production.sh --skip-build # Nur Service-Neustart +./scripts/deploy-production.sh --rollback # Rollback zur vorherigen Version +./scripts/deploy-production.sh --dry-run # Zeigt was passieren würde ``` -### Deploy-Script (~/deploy.sh) +### Rollback bei Problemen ```bash -#!/bin/bash -set -e +# Automatischer Rollback zur vorherigen Version +./scripts/deploy-production.sh --rollback -cd ~/payload-cms - -echo "📥 Pulling latest changes..." -git pull origin main - -echo "📦 Installing dependencies..." -pnpm install - -echo "🔄 Running migrations..." -pnpm payload migrate - -echo "🏗️ Building..." -pnpm build - -echo "🔄 Restarting PM2..." -pm2 restart payload - -echo "✅ Deployment complete!" -pm2 status -``` - -### Manuelles Deployment - -```bash -ssh payload@162.55.85.18 -cd ~/payload-cms -git pull origin main -pnpm install -pnpm payload migrate +# Oder manuell zu spezifischem Commit +git log --oneline -10 +git reset --hard +pnpm install --frozen-lockfile pnpm build pm2 restart payload ``` @@ -396,15 +396,17 @@ TRUST_PROXY=true | Workflow | Trigger | Aktion | |----------|---------|--------| -| `ci.yml` | Push/PR auf main, develop | Lint, Test, Build | +| `ci.yml` | Push/PR auf main, develop | Lint, Test, Build, E2E | | `security.yml` | Push/PR, Schedule | Security Scanning | | `deploy-staging.yml` | Push auf develop | Auto-Deploy zu Staging | +| `deploy-production.yml` | Manuell (workflow_dispatch) | Production Deployment | ### Secrets (GitHub) | Secret | Beschreibung | |--------|--------------| | `STAGING_SSH_KEY` | SSH Private Key für sv-payload | +| `PRODUCTION_SSH_KEY` | SSH Private Key für Hetzner 3 | ### Manuelles Deployment triggern @@ -522,4 +524,4 @@ pnpm payload migrate --name 20251216_073000_add_video_collections --- -*Dokumentation: Complex Care Solutions GmbH | 18.12.2025* +*Dokumentation: Complex Care Solutions GmbH | 29.12.2025* diff --git a/docs/DEPLOYMENT_STRATEGY.md b/docs/DEPLOYMENT_STRATEGY.md index 13dcd77..ba99a2a 100644 --- a/docs/DEPLOYMENT_STRATEGY.md +++ b/docs/DEPLOYMENT_STRATEGY.md @@ -1,6 +1,8 @@ # Deployment-Strategie: Dev → Production -*Erstellt: 27. Dezember 2025* +*Erstellt: 27. Dezember 2025 | Aktualisiert: 29. Dezember 2025* + +> **Siehe auch:** [STAGING-DEPLOYMENT.md](./STAGING-DEPLOYMENT.md) für detaillierte Staging-Workflow-Dokumentation ## Zusammenfassung diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md index 537c0c2..324e2ce 100644 --- a/docs/INFRASTRUCTURE.md +++ b/docs/INFRASTRUCTURE.md @@ -1,6 +1,6 @@ # Infrastruktur Dokumentation -*Letzte Aktualisierung: 18. Dezember 2025* +*Letzte Aktualisierung: 29. Dezember 2025* ## Gesamtübersicht @@ -84,10 +84,10 @@ ### Software Stack - Node.js 22.x - pnpm -- Next.js 16.0.10 -- Claude Code 2.0.72 -- Codex CLI 0.73.0 -- Gemini CLI 0.21.2 +- Next.js 15.5.9 +- Claude Code (aktuell) +- Codex CLI (aktuell) +- Gemini CLI (aktuell) ### Projekte & Ports @@ -239,4 +239,4 @@ systemctl start frontend-porwoll --- -*Dokumentation: Martin Porwoll | Complex Care Solutions GmbH | 18.12.2025* +*Dokumentation: Martin Porwoll | Complex Care Solutions GmbH | 29.12.2025* diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index 11d0f05..5f43319 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,6 @@ # Projekt Status - Dezember 2025 -**Stand:** 27. Dezember 2025 +**Stand:** 29. Dezember 2025 ## Zusammenfassung @@ -177,6 +177,15 @@ pm2 logs payload ## 📝 Änderungsprotokoll +### 29.12.2025 +- **Dokumentation konsolidiert und aktualisiert:** + - CLAUDE.md: Collections (40+) und Blocks (37) vollständig dokumentiert + - CLAUDE.md: Neue Collections (Products, Tags, Authors, Locations, Partners, Jobs, Downloads, Events) + - CLAUDE.md: Neue Blocks (ImageSlider, Blogging, Team-Filter, Feature-Blocks, Interactive-Blocks) + - CLAUDE.md: Globals-Abschnitt korrigiert (SiteSettings/Navigations sind jetzt Collections) + - docs/INFRASTRUCTURE.md, docs/DEPLOYMENT.md aktualisiert + - docs/anleitungen/*.md auf aktuelle URLs korrigiert + ### 27.12.2025 - Payload CMS Update 3.68.4 → 3.69.0 - Bug-Fixes Admin Panel: diff --git a/docs/STAGING-DEPLOYMENT.md b/docs/STAGING-DEPLOYMENT.md index 130cf91..6834564 100644 --- a/docs/STAGING-DEPLOYMENT.md +++ b/docs/STAGING-DEPLOYMENT.md @@ -1,9 +1,11 @@ # Staging Deployment -> **Staging URL:** https://pl.c2sgmbh.de +> **Staging URL:** https://pl.porwoll.tech > **Server:** sv-payload (37.24.237.181) > **Branch:** `develop` +> **Siehe auch:** [DEPLOYMENT_STRATEGY.md](./DEPLOYMENT_STRATEGY.md) für die vollständige Deployment-Strategie (Dev → Prod) + --- ## Übersicht @@ -14,7 +16,7 @@ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐│ │ │ Developer │ │ GitHub │ │ Staging Server ││ -│ │ │ │ Actions │ │ pl.c2sgmbh.de ││ +│ │ │ │ Actions │ │ pl.porwoll.tech ││ │ └──────┬───────┘ └──────┬───────┘ └──────────────┬───────────────┘│ │ │ │ │ │ │ │ git push │ │ │ @@ -80,7 +82,7 @@ Jobs: deploy ### 3. Verify Deployment -- HTTP-Status von https://pl.c2sgmbh.de/admin prüfen +- HTTP-Status von https://pl.porwoll.tech/admin prüfen - Bei Fehler: Benachrichtigung im Workflow-Summary --- @@ -233,7 +235,7 @@ main (Produktion) | Branch | Deployment | URL | |--------|------------|-----| | `main` | Produktion (manuell) | cms.c2sgmbh.de | -| `develop` | Staging (automatisch) | pl.c2sgmbh.de | +| `develop` | Staging (automatisch) | pl.porwoll.tech | --- @@ -249,4 +251,4 @@ gh run list --workflow=deploy-staging.yml --limit=5 --- -*Letzte Aktualisierung: 14.12.2025* +*Letzte Aktualisierung: 29.12.2025* diff --git a/docs/anleitungen/API_ANLEITUNG.md b/docs/anleitungen/API_ANLEITUNG.md index 7ec318e..8d71ac7 100644 --- a/docs/anleitungen/API_ANLEITUNG.md +++ b/docs/anleitungen/API_ANLEITUNG.md @@ -4,7 +4,7 @@ Das Payload CMS stellt eine REST-API und eine GraphQL-API bereit. Diese Anleitung beschreibt die Nutzung der REST-API für alle Collections. -**Base URL:** `https://pl.c2sgmbh.de/api` +**Base URL:** `https://pl.porwoll.tech/api` --- @@ -13,7 +13,7 @@ Das Payload CMS stellt eine REST-API und eine GraphQL-API bereit. Diese Anleitun ### Login ```bash -curl -X POST "https://pl.c2sgmbh.de/api/users/login" \ +curl -X POST "https://pl.porwoll.tech/api/users/login" \ -H "Content-Type: application/json" \ -d '{ "email": "admin@example.com", @@ -38,7 +38,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/users/login" \ ### Token verwenden ```bash -curl "https://pl.c2sgmbh.de/api/posts" \ +curl "https://pl.porwoll.tech/api/posts" \ -H "Authorization: JWT eyJhbGciOiJIUzI1NiIs..." ``` @@ -50,13 +50,13 @@ Das CMS unterstützt Deutsch (de) und Englisch (en). Lokalisierte Felder können ```bash # Deutsche Inhalte (Standard) -curl "https://pl.c2sgmbh.de/api/posts?locale=de" +curl "https://pl.porwoll.tech/api/posts?locale=de" # Englische Inhalte -curl "https://pl.c2sgmbh.de/api/posts?locale=en" +curl "https://pl.porwoll.tech/api/posts?locale=en" # Alle Sprachen gleichzeitig -curl "https://pl.c2sgmbh.de/api/posts?locale=all" +curl "https://pl.porwoll.tech/api/posts?locale=all" ``` --- @@ -66,7 +66,7 @@ curl "https://pl.c2sgmbh.de/api/posts?locale=all" ### Aktuellen User abrufen ```bash -curl "https://pl.c2sgmbh.de/api/users/me" \ +curl "https://pl.porwoll.tech/api/users/me" \ -H "Authorization: JWT your-token" ``` @@ -85,14 +85,14 @@ curl "https://pl.c2sgmbh.de/api/users/me" \ ### Alle Tenants abrufen (Auth + SuperAdmin erforderlich) ```bash -curl "https://pl.c2sgmbh.de/api/tenants" \ +curl "https://pl.porwoll.tech/api/tenants" \ -H "Authorization: JWT your-token" ``` ### Tenant erstellen (Auth + SuperAdmin erforderlich) ```bash -curl -X POST "https://pl.c2sgmbh.de/api/tenants" \ +curl -X POST "https://pl.porwoll.tech/api/tenants" \ -H "Authorization: JWT your-token" \ -H "Content-Type: application/json" \ -d '{ @@ -121,47 +121,47 @@ curl -X POST "https://pl.c2sgmbh.de/api/tenants" \ ```bash # Alle Posts -curl "https://pl.c2sgmbh.de/api/posts" +curl "https://pl.porwoll.tech/api/posts" # Nur Blog-Artikel -curl "https://pl.c2sgmbh.de/api/posts?where[type][equals]=blog" +curl "https://pl.porwoll.tech/api/posts?where[type][equals]=blog" # Nur News -curl "https://pl.c2sgmbh.de/api/posts?where[type][equals]=news" +curl "https://pl.porwoll.tech/api/posts?where[type][equals]=news" # Nur veröffentlichte Posts -curl "https://pl.c2sgmbh.de/api/posts?where[status][equals]=published" +curl "https://pl.porwoll.tech/api/posts?where[status][equals]=published" # Nur hervorgehobene Posts -curl "https://pl.c2sgmbh.de/api/posts?where[isFeatured][equals]=true" +curl "https://pl.porwoll.tech/api/posts?where[isFeatured][equals]=true" # Mit Sortierung (neueste zuerst) -curl "https://pl.c2sgmbh.de/api/posts?sort=-publishedAt" +curl "https://pl.porwoll.tech/api/posts?sort=-publishedAt" # Limitiert auf 10 Einträge -curl "https://pl.c2sgmbh.de/api/posts?limit=10" +curl "https://pl.porwoll.tech/api/posts?limit=10" # Pagination (Seite 2) -curl "https://pl.c2sgmbh.de/api/posts?limit=10&page=2" +curl "https://pl.porwoll.tech/api/posts?limit=10&page=2" # Mit Locale -curl "https://pl.c2sgmbh.de/api/posts?locale=de" +curl "https://pl.porwoll.tech/api/posts?locale=de" ``` ### Einzelnen Post abrufen ```bash # Nach ID -curl "https://pl.c2sgmbh.de/api/posts/1" +curl "https://pl.porwoll.tech/api/posts/1" # Nach Slug (über Query) -curl "https://pl.c2sgmbh.de/api/posts?where[slug][equals]=mein-erster-artikel" +curl "https://pl.porwoll.tech/api/posts?where[slug][equals]=mein-erster-artikel" ``` ### Post erstellen (Auth erforderlich) ```bash -curl -X POST "https://pl.c2sgmbh.de/api/posts" \ +curl -X POST "https://pl.porwoll.tech/api/posts" \ -H "Authorization: JWT your-token" \ -H "Content-Type: application/json" \ -d '{ @@ -189,7 +189,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/posts" \ ### Post aktualisieren (Auth erforderlich) ```bash -curl -X PATCH "https://pl.c2sgmbh.de/api/posts/1" \ +curl -X PATCH "https://pl.porwoll.tech/api/posts/1" \ -H "Authorization: JWT your-token" \ -H "Content-Type: application/json" \ -d '{ @@ -201,7 +201,7 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/posts/1" \ ### Post löschen (Auth erforderlich) ```bash -curl -X DELETE "https://pl.c2sgmbh.de/api/posts/1" \ +curl -X DELETE "https://pl.porwoll.tech/api/posts/1" \ -H "Authorization: JWT your-token" ``` @@ -213,22 +213,22 @@ curl -X DELETE "https://pl.c2sgmbh.de/api/posts/1" \ ```bash # Alle Testimonials -curl "https://pl.c2sgmbh.de/api/testimonials" +curl "https://pl.porwoll.tech/api/testimonials" # Nur aktive Testimonials -curl "https://pl.c2sgmbh.de/api/testimonials?where[isActive][equals]=true" +curl "https://pl.porwoll.tech/api/testimonials?where[isActive][equals]=true" # Sortiert nach Bewertung (beste zuerst) -curl "https://pl.c2sgmbh.de/api/testimonials?sort=-rating" +curl "https://pl.porwoll.tech/api/testimonials?sort=-rating" # Sortiert nach eigener Reihenfolge -curl "https://pl.c2sgmbh.de/api/testimonials?sort=order" +curl "https://pl.porwoll.tech/api/testimonials?sort=order" ``` ### Testimonial erstellen (Auth erforderlich) ```bash -curl -X POST "https://pl.c2sgmbh.de/api/testimonials" \ +curl -X POST "https://pl.porwoll.tech/api/testimonials" \ -H "Authorization: JWT your-token" \ -H "Content-Type: application/json" \ -d '{ @@ -252,7 +252,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/testimonials" \ ```bash # Einfache Anmeldung -curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \ +curl -X POST "https://pl.porwoll.tech/api/newsletter-subscribers" \ -H "Content-Type: application/json" \ -d '{ "tenant": 1, @@ -261,7 +261,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \ }' # Mit Namen und Interessen -curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \ +curl -X POST "https://pl.porwoll.tech/api/newsletter-subscribers" \ -H "Content-Type: application/json" \ -d '{ "tenant": 1, @@ -292,15 +292,15 @@ curl -X POST "https://pl.c2sgmbh.de/api/newsletter-subscribers" \ ```bash # Alle Subscribers -curl "https://pl.c2sgmbh.de/api/newsletter-subscribers" \ +curl "https://pl.porwoll.tech/api/newsletter-subscribers" \ -H "Authorization: JWT your-token" # Nur bestätigte Subscribers -curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[status][equals]=confirmed" \ +curl "https://pl.porwoll.tech/api/newsletter-subscribers?where[status][equals]=confirmed" \ -H "Authorization: JWT your-token" # Nach E-Mail suchen -curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[email][equals]=kunde@example.com" \ +curl "https://pl.porwoll.tech/api/newsletter-subscribers?where[email][equals]=kunde@example.com" \ -H "Authorization: JWT your-token" ``` @@ -308,7 +308,7 @@ curl "https://pl.c2sgmbh.de/api/newsletter-subscribers?where[email][equals]=kund ```bash # Über Token bestätigen -curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \ +curl -X PATCH "https://pl.porwoll.tech/api/newsletter-subscribers/1" \ -H "Authorization: JWT your-token" \ -H "Content-Type: application/json" \ -d '{ @@ -319,7 +319,7 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \ ### Subscriber abmelden ```bash -curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \ +curl -X PATCH "https://pl.porwoll.tech/api/newsletter-subscribers/1" \ -H "Authorization: JWT your-token" \ -H "Content-Type: application/json" \ -d '{ @@ -335,16 +335,16 @@ curl -X PATCH "https://pl.c2sgmbh.de/api/newsletter-subscribers/1" \ ```bash # Alle Seiten -curl "https://pl.c2sgmbh.de/api/pages" +curl "https://pl.porwoll.tech/api/pages" # Seite nach Slug -curl "https://pl.c2sgmbh.de/api/pages?where[slug][equals]=startseite" +curl "https://pl.porwoll.tech/api/pages?where[slug][equals]=startseite" # Nur veröffentlichte Seiten -curl "https://pl.c2sgmbh.de/api/pages?where[status][equals]=published" +curl "https://pl.porwoll.tech/api/pages?where[status][equals]=published" # Mit Locale -curl "https://pl.c2sgmbh.de/api/pages?locale=de&depth=2" +curl "https://pl.porwoll.tech/api/pages?locale=de&depth=2" ``` ### Seite mit Blocks @@ -389,7 +389,7 @@ Die Cookie-Konfiguration wird automatisch nach Domain gefiltert. ```bash # Für Frontend Cookie-Banner -curl "https://pl.c2sgmbh.de/api/cookie-configurations?where[tenant][equals]=1" +curl "https://pl.porwoll.tech/api/cookie-configurations?where[tenant][equals]=1" ``` **Response:** @@ -432,13 +432,13 @@ Dokumentation aller verwendeten Cookies für die Datenschutzerklärung. ```bash # Alle Cookies eines Tenants -curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1" +curl "https://pl.porwoll.tech/api/cookie-inventory?where[tenant][equals]=1" # Nur aktive Cookies -curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1&where[isActive][equals]=true" +curl "https://pl.porwoll.tech/api/cookie-inventory?where[tenant][equals]=1&where[isActive][equals]=true" # Nach Kategorie filtern -curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1&where[category][equals]=analytics" +curl "https://pl.porwoll.tech/api/cookie-inventory?where[tenant][equals]=1&where[category][equals]=analytics" ``` **Response:** @@ -473,7 +473,7 @@ curl "https://pl.c2sgmbh.de/api/cookie-inventory?where[tenant][equals]=1&where[c Consent-Logs sind ein WORM (Write-Once-Read-Many) Audit-Trail für DSGVO-Nachweise. ```bash -curl -X POST "https://pl.c2sgmbh.de/api/consent-logs" \ +curl -X POST "https://pl.porwoll.tech/api/consent-logs" \ -H "Content-Type: application/json" \ -H "X-API-Key: your-consent-api-key" \ -d '{ @@ -531,7 +531,7 @@ curl -X POST "https://pl.c2sgmbh.de/api/consent-logs" \ Konfiguration für die Datenschutzerklärungs-Seite (z.B. Alfright Integration). ```bash -curl "https://pl.c2sgmbh.de/api/privacy-policy-settings?where[tenant][equals]=1" +curl "https://pl.porwoll.tech/api/privacy-policy-settings?where[tenant][equals]=1" ``` **Response:** @@ -693,7 +693,7 @@ curl "https://pl.c2sgmbh.de/api/privacy-policy-settings?where[tenant][equals]=1" 1. **Admin Panel:** Unter "Settings → Tenants" die ID ablesen 2. **API:** ```bash -curl "https://pl.c2sgmbh.de/api/tenants" \ +curl "https://pl.porwoll.tech/api/tenants" \ -H "Authorization: JWT your-token" ``` @@ -705,7 +705,7 @@ Wenn ein Frontend über eine Tenant-Domain (z.B. `porwoll.de`) zugreift, wird de Für direkte API-Zugriffe: ```bash -curl "https://pl.c2sgmbh.de/api/posts?where[tenant][equals]=1" +curl "https://pl.porwoll.tech/api/posts?where[tenant][equals]=1" ``` ### Super Admin @@ -720,7 +720,7 @@ User mit `isSuperAdmin: true` haben Zugriff auf alle Tenants und können neue Te ```typescript // lib/api.ts -const API_BASE = 'https://pl.c2sgmbh.de/api' +const API_BASE = 'https://pl.porwoll.tech/api' const TENANT_ID = 1 export async function getPosts(type?: string, limit = 10, locale = 'de') { @@ -889,6 +889,6 @@ Aktuell gibt es kein Rate Limiting. Für Production-Umgebungen sollte ein Revers ## Weitere Ressourcen -- **Admin Panel:** https://pl.c2sgmbh.de/admin +- **Admin Panel:** https://pl.porwoll.tech/admin - **Payload CMS Docs:** https://payloadcms.com/docs -- **GraphQL Playground:** https://pl.c2sgmbh.de/api/graphql (wenn aktiviert) +- **GraphQL Playground:** https://pl.porwoll.tech/api/graphql (wenn aktiviert) diff --git a/docs/anleitungen/SECURITY.md b/docs/anleitungen/SECURITY.md index 61b8cd2..c1ceff5 100644 --- a/docs/anleitungen/SECURITY.md +++ b/docs/anleitungen/SECURITY.md @@ -1,6 +1,6 @@ # Security-Richtlinien - Payload CMS Multi-Tenant -> Letzte Aktualisierung: 18.12.2025 +> Letzte Aktualisierung: 29.12.2025 ## Übersicht @@ -323,29 +323,13 @@ email=admin@example.com&password=secret - Rate-Limiting verhindert Brute-Force-Angriffe - Open Redirect Prevention durch URL-Validierung -### Custom Admin Login Page - -Eine optionale Custom Login-Seite ist verfügbar unter `src/app/(payload)/admin/login/`: - -``` -src/app/(payload)/admin/login/ -├── page.tsx # Login-Formular mit Styling -└── page.module.scss # Custom Styles -``` - -**Features:** -- Styled Login-Form passend zum Admin-Theme -- Redirect-Parameter Support (`?redirect=/admin/...`) -- Fehlerbehandlung mit User-Feedback -- Kompatibel mit Payload's Session-Management - --- ## Änderungshistorie | Datum | Änderung | |-------|----------| -| 18.12.2025 | **Custom Admin Login Page:** Styled Login-Formular, Browser-Redirect mit Safe-URL-Validierung, Open Redirect Prevention | +| 29.12.2025 | **Dokumentation aktualisiert:** Custom Login Page Abschnitt entfernt (wurde am 27.12.2025 entfernt) | | 17.12.2025 | **Security-Audit Fixes:** TRUST_PROXY für IP-Header-Spoofing, CSRF_SECRET Pflicht in Production, IP-Allowlist Startup-Warnungen, Tests auf 177 erweitert | | 09.12.2025 | Custom Login Route Dokumentation, multipart/form-data _payload Support | | 08.12.2025 | Security Test Suite (143 Tests) | @@ -362,8 +346,7 @@ src/app/(payload)/admin/login/ | `src/lib/security/ip-allowlist.ts` | IP-basierte Zugriffskontrolle | | `src/lib/security/csrf.ts` | CSRF Token Generation & Validation | | `src/lib/security/data-masking.ts` | Sensitive Data Masking | -| `src/app/(payload)/api/users/login/route.ts` | Custom Login API | -| `src/app/(payload)/admin/login/page.tsx` | Custom Login Page | +| `src/app/(payload)/api/users/login/route.ts` | Custom Login API mit Audit | | `scripts/detect-secrets.sh` | Pre-Commit Secret Detection | | `.github/workflows/security.yml` | CI Security Scanning | | `tests/unit/security/` | Security Unit Tests | diff --git a/docs/anleitungen/TODO.md b/docs/anleitungen/TODO.md index 711a1d2..f04cef1 100644 --- a/docs/anleitungen/TODO.md +++ b/docs/anleitungen/TODO.md @@ -222,13 +222,31 @@ --- -*Letzte Aktualisierung: 27.12.2025* +*Letzte Aktualisierung: 29.12.2025* --- ## Changelog +### 29.12.2025 +- **Dokumentation konsolidiert und aktualisiert:** + - CLAUDE.md: Collections (40+) und Blocks (37) vollständig dokumentiert + - CLAUDE.md: Neue Collections hinzugefügt (Products, Tags, Authors, Locations, Partners, Jobs, Downloads, Events) + - CLAUDE.md: Neue Blocks hinzugefügt (ImageSlider, Blogging, Team-Filter, Feature, Interactive Blocks) + - CLAUDE.md: Globals-Abschnitt korrigiert (SiteSettings/Navigations sind jetzt Collections) + - docs/INFRASTRUCTURE.md aktualisiert + - docs/DEPLOYMENT.md aktualisiert + - docs/STAGING-DEPLOYMENT.md: URLs korrigiert (pl.porwoll.tech) + - docs/anleitungen/API_ANLEITUNG.md: URLs korrigiert (pl.porwoll.tech) + - docs/anleitungen/SECURITY.md: Custom Login Page Abschnitt entfernt + ### 27.12.2025 +- **Production Deployment-Strategie implementiert:** + - GitHub Actions Workflow `deploy-production.yml` für manuelles Production-Deployment + - Deploy-Script `scripts/deploy-production.sh` mit Backup, Health Check, Rollback + - Umfassende Dokumentation in `docs/DEPLOYMENT_STRATEGY.md` + - Features: Pre-flight Checks, DB-Backup, automatischer Rollback bei Fehler + - Neues GitHub Secret erforderlich: `PRODUCTION_SSH_KEY` - **Payload CMS Update 3.68.4 → 3.69.0:** - Login Redirect Loop behoben (formatAdminURL generiert keine absoluten URLs mehr) - Update aller @payloadcms/* Pakete diff --git a/src/blocks/FavoritesBlock.ts b/src/blocks/FavoritesBlock.ts new file mode 100644 index 0000000..53ae96b --- /dev/null +++ b/src/blocks/FavoritesBlock.ts @@ -0,0 +1,182 @@ +import type { Block } from 'payload' + +/** + * Background color options (shared with other blocks) + */ +const backgroundColorOptions = [ + { label: 'Weiß', value: 'white' }, + { label: 'Ivory', value: 'ivory' }, + { label: 'Sand', value: 'sand' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, +] + +/** + * Category options matching the Favorites collection + */ +const categoryFilterOptions = [ + { label: 'Alle Kategorien', value: 'all' }, + { label: 'Fashion', value: 'fashion' }, + { label: 'Beauty', value: 'beauty' }, + { label: 'Travel', value: 'travel' }, + { label: 'Tech', value: 'tech' }, + { label: 'Home', value: 'home' }, +] + +/** + * FavoritesBlock + * + * Zeigt Favoriten/Affiliate-Produkte aus der Favorites Collection. + * Unterstützt verschiedene Layouts und Filteroptionen. + */ +export const FavoritesBlock: Block = { + slug: 'favorites-block', + labels: { + singular: 'Favoriten', + plural: 'Favoriten', + }, + imageURL: '/assets/blocks/favorites.png', + fields: [ + // Header + { + name: 'title', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + + // Filtering + { + name: 'category', + type: 'select', + defaultValue: 'all', + options: categoryFilterOptions, + label: 'Kategorie-Filter', + admin: { + description: 'Nur Favoriten dieser Kategorie anzeigen', + }, + }, + { + name: 'showFeaturedOnly', + type: 'checkbox', + defaultValue: false, + label: 'Nur Featured anzeigen', + admin: { + description: 'Nur als "Featured" markierte Favoriten anzeigen', + }, + }, + { + name: 'limit', + type: 'number', + defaultValue: 8, + min: 1, + max: 50, + label: 'Anzahl', + admin: { + description: 'Maximale Anzahl der angezeigten Favoriten', + }, + }, + + // Layout + { + name: 'layout', + type: 'select', + defaultValue: 'grid', + label: 'Layout', + options: [ + { label: 'Grid', value: 'grid' }, + { label: 'Liste', value: 'list' }, + { label: 'Karussell', value: 'carousel' }, + ], + }, + { + name: 'columns', + type: 'select', + defaultValue: '4', + label: 'Spalten', + options: [ + { label: '2 Spalten', value: '2' }, + { label: '3 Spalten', value: '3' }, + { label: '4 Spalten', value: '4' }, + ], + admin: { + condition: (_, siblingData) => + siblingData?.layout === 'grid' || siblingData?.layout === 'carousel', + }, + }, + + // Display Options + { + name: 'showPrice', + type: 'checkbox', + defaultValue: true, + label: 'Preis anzeigen', + }, + { + name: 'showBadge', + type: 'checkbox', + defaultValue: true, + label: 'Badge anzeigen', + }, + { + name: 'showDescription', + type: 'checkbox', + defaultValue: false, + label: 'Beschreibung anzeigen', + }, + { + name: 'showCategory', + type: 'checkbox', + defaultValue: false, + label: 'Kategorie anzeigen', + }, + + // Styling + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'white', + options: backgroundColorOptions, + label: 'Hintergrundfarbe', + }, + + // CTA + { + name: 'cta', + type: 'group', + label: 'Call-to-Action', + fields: [ + { + name: 'showCta', + type: 'checkbox', + defaultValue: false, + label: 'CTA-Button anzeigen', + }, + { + name: 'ctaText', + type: 'text', + label: 'Button-Text', + defaultValue: 'Alle Favoriten ansehen', + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.showCta, + }, + }, + { + name: 'ctaUrl', + type: 'text', + label: 'Button-Link', + admin: { + condition: (_, siblingData) => siblingData?.showCta, + }, + }, + ], + }, + ], +} diff --git a/src/blocks/FeaturedContentBlock.ts b/src/blocks/FeaturedContentBlock.ts new file mode 100644 index 0000000..9655696 --- /dev/null +++ b/src/blocks/FeaturedContentBlock.ts @@ -0,0 +1,342 @@ +import type { Block } from 'payload' + +/** + * Background color options + */ +const backgroundColorOptions = [ + { label: 'Weiß', value: 'white' }, + { label: 'Ivory', value: 'ivory' }, + { label: 'Sand', value: 'sand' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, +] + +/** + * Item Type options + */ +const itemTypeOptions = [ + { label: 'Blog-Beitrag', value: 'post' }, + { label: 'Video', value: 'video' }, + { label: 'Serie', value: 'series' }, + { label: 'Externer Link', value: 'external' }, +] + +/** + * FeaturedContentBlock + * + * Kuratierte Sammlung von verschiedenen Content-Typen. + * Ermöglicht das Mischen von Posts, Videos, Serien und externen Links. + */ +export const FeaturedContentBlock: Block = { + slug: 'featured-content-block', + labels: { + singular: 'Featured Content', + plural: 'Featured Contents', + }, + imageURL: '/assets/blocks/featured-content.png', + fields: [ + // Header + { + name: 'title', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + + // Items Array + { + name: 'items', + type: 'array', + required: true, + minRows: 1, + maxRows: 12, + label: 'Inhalte', + admin: { + description: 'Wähle verschiedene Content-Typen aus', + }, + fields: [ + // Item Type Selection + { + name: 'itemType', + type: 'select', + required: true, + options: itemTypeOptions, + label: 'Typ', + }, + + // Post Selection (condition: itemType === 'post') + { + name: 'post', + type: 'relationship', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + relationTo: 'posts' as any, + label: 'Beitrag auswählen', + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'post', + }, + }, + + // Video Selection (condition: itemType === 'video') + { + name: 'video', + type: 'relationship', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + relationTo: 'videos' as any, + label: 'Video auswählen', + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'video', + }, + }, + + // Series Selection (condition: itemType === 'series') + { + name: 'series', + type: 'relationship', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + relationTo: 'series' as any, + label: 'Serie auswählen', + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'series', + }, + }, + + // External Content (condition: itemType === 'external') + { + name: 'externalTitle', + type: 'text', + label: 'Titel', + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'external', + }, + }, + { + name: 'externalUrl', + type: 'text', + label: 'URL', + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'external', + }, + validate: ( + value: string | undefined | null, + { siblingData }: { siblingData?: Record } + ) => { + if (siblingData?.itemType !== 'external') return true + if (!value) return 'URL ist erforderlich' + try { + new URL(value) + return true + } catch { + return 'Bitte eine gültige URL eingeben' + } + }, + }, + { + name: 'externalImage', + type: 'upload', + relationTo: 'media', + label: 'Bild', + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'external', + }, + }, + { + name: 'externalDescription', + type: 'textarea', + label: 'Beschreibung', + maxLength: 200, + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.itemType === 'external', + }, + }, + + // Custom Label (for all types) + { + name: 'customLabel', + type: 'text', + label: 'Custom-Label', + localized: true, + admin: { + description: 'Optionales Label (z.B. "NEU", "TRENDING", "MUSS-LESEN")', + }, + }, + + // Custom Order/Featured + { + name: 'featured', + type: 'checkbox', + defaultValue: false, + label: 'Hervorgehoben', + admin: { + description: 'In featured-grid Layout größer darstellen', + }, + }, + ], + }, + + // Layout + { + name: 'layout', + type: 'select', + defaultValue: 'grid', + label: 'Layout', + options: [ + { label: 'Grid', value: 'grid' }, + { label: 'Karussell', value: 'carousel' }, + { label: 'Liste', value: 'list' }, + { label: 'Featured Grid (erstes Element größer)', value: 'featured-grid' }, + ], + }, + { + name: 'columns', + type: 'select', + defaultValue: '3', + label: 'Spalten', + options: [ + { label: '2 Spalten', value: '2' }, + { label: '3 Spalten', value: '3' }, + { label: '4 Spalten', value: '4' }, + ], + admin: { + condition: (_, siblingData) => + siblingData?.layout === 'grid' || + siblingData?.layout === 'carousel' || + siblingData?.layout === 'featured-grid', + }, + }, + + // Display Options + { + name: 'showDates', + type: 'checkbox', + defaultValue: true, + label: 'Datum anzeigen', + }, + { + name: 'showType', + type: 'checkbox', + defaultValue: true, + label: 'Content-Typ anzeigen', + admin: { + description: 'Icon oder Label für Post/Video/Serie anzeigen', + }, + }, + { + name: 'showDescription', + type: 'checkbox', + defaultValue: true, + label: 'Beschreibung anzeigen', + }, + { + name: 'showCustomLabels', + type: 'checkbox', + defaultValue: true, + label: 'Custom-Labels anzeigen', + }, + + // Styling + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'white', + options: backgroundColorOptions, + label: 'Hintergrundfarbe', + }, + + // Card Style + { + name: 'card', + type: 'group', + label: 'Karten-Stil', + fields: [ + { + name: 'bg', + type: 'select', + defaultValue: 'white', + label: 'Karten-Hintergrund', + options: [ + { label: 'Weiß', value: 'white' }, + { label: 'Transparent', value: 'transparent' }, + { label: 'Hell', value: 'light' }, + ], + }, + { + name: 'shadow', + type: 'checkbox', + defaultValue: true, + label: 'Schatten', + }, + { + name: 'border', + type: 'checkbox', + defaultValue: false, + label: 'Rahmen', + }, + { + name: 'imgRatio', + type: 'select', + defaultValue: '16:9', + label: 'Bild-Verhältnis', + options: [ + { label: '16:9', value: '16:9' }, + { label: '4:3', value: '4:3' }, + { label: '1:1 (Quadrat)', value: '1:1' }, + { label: '3:2', value: '3:2' }, + ], + }, + { + name: 'hover', + type: 'select', + defaultValue: 'lift', + label: 'Hover-Effekt', + options: [ + { label: 'Keiner', value: 'none' }, + { label: 'Anheben', value: 'lift' }, + { label: 'Zoom (Bild)', value: 'zoom' }, + { label: 'Schatten verstärken', value: 'shadow' }, + ], + }, + ], + }, + + // CTA + { + name: 'cta', + type: 'group', + label: 'Call-to-Action', + fields: [ + { + name: 'showCta', + type: 'checkbox', + defaultValue: false, + label: 'CTA-Button anzeigen', + }, + { + name: 'ctaText', + type: 'text', + label: 'Button-Text', + defaultValue: 'Alle ansehen', + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.showCta, + }, + }, + { + name: 'ctaUrl', + type: 'text', + label: 'Button-Link', + admin: { + condition: (_, siblingData) => siblingData?.showCta, + }, + }, + ], + }, + ], +} diff --git a/src/blocks/SeriesBlock.ts b/src/blocks/SeriesBlock.ts new file mode 100644 index 0000000..399e51a --- /dev/null +++ b/src/blocks/SeriesBlock.ts @@ -0,0 +1,154 @@ +import type { Block } from 'payload' + +/** + * Background color options + */ +const backgroundColorOptions = [ + { label: 'Weiß', value: 'white' }, + { label: 'Ivory', value: 'ivory' }, + { label: 'Sand', value: 'sand' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, +] + +/** + * SeriesBlock + * + * Zeigt Serien aus der Series Collection. + * Verschiedene Layouts für Übersichtsseiten. + */ +export const SeriesBlock: Block = { + slug: 'series-block', + labels: { + singular: 'Serien-Übersicht', + plural: 'Serien-Übersichten', + }, + imageURL: '/assets/blocks/series.png', + fields: [ + // Header + { + name: 'title', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + + // Layout + { + name: 'layout', + type: 'select', + defaultValue: 'grid', + label: 'Layout', + options: [ + { label: 'Grid', value: 'grid' }, + { label: 'Liste', value: 'list' }, + { label: 'Featured (Hero + Grid)', value: 'featured' }, + ], + }, + { + name: 'columns', + type: 'select', + defaultValue: '3', + label: 'Spalten', + options: [ + { label: '2 Spalten', value: '2' }, + { label: '3 Spalten', value: '3' }, + { label: '4 Spalten', value: '4' }, + ], + admin: { + condition: (_, siblingData) => + siblingData?.layout === 'grid' || siblingData?.layout === 'featured', + }, + }, + + // Display Options + { + name: 'showDescription', + type: 'checkbox', + defaultValue: true, + label: 'Beschreibung anzeigen', + }, + { + name: 'showLogo', + type: 'checkbox', + defaultValue: true, + label: 'Logo anzeigen', + }, + { + name: 'showTagline', + type: 'checkbox', + defaultValue: true, + label: 'Tagline anzeigen', + }, + { + name: 'useBrandColors', + type: 'checkbox', + defaultValue: true, + label: 'Markenfarben verwenden', + admin: { + description: 'Serien-spezifische Farben für Akzente nutzen', + }, + }, + + // Filtering + { + name: 'limit', + type: 'number', + defaultValue: 6, + min: 1, + max: 20, + label: 'Anzahl', + admin: { + description: 'Maximale Anzahl der angezeigten Serien', + }, + }, + + // Styling + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'white', + options: backgroundColorOptions, + label: 'Hintergrundfarbe', + }, + + // CTA + { + name: 'cta', + type: 'group', + label: 'Call-to-Action', + fields: [ + { + name: 'showCta', + type: 'checkbox', + defaultValue: false, + label: 'CTA-Button anzeigen', + }, + { + name: 'ctaText', + type: 'text', + label: 'Button-Text', + defaultValue: 'Alle Serien ansehen', + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.showCta, + }, + }, + { + name: 'ctaUrl', + type: 'text', + label: 'Button-Link', + admin: { + condition: (_, siblingData) => siblingData?.showCta, + }, + }, + ], + }, + ], +} diff --git a/src/blocks/SeriesDetailBlock.ts b/src/blocks/SeriesDetailBlock.ts new file mode 100644 index 0000000..cd1eb51 --- /dev/null +++ b/src/blocks/SeriesDetailBlock.ts @@ -0,0 +1,155 @@ +import type { Block } from 'payload' + +/** + * SeriesDetailBlock + * + * Für Serien-Einzelseiten mit Hero und Content. + * Zeigt die ausgewählte Serie mit Hero, Beschreibung und verwandten Posts. + */ +export const SeriesDetailBlock: Block = { + slug: 'series-detail-block', + labels: { + singular: 'Serien-Detail', + plural: 'Serien-Details', + }, + imageURL: '/assets/blocks/series-detail.png', + fields: [ + // Series Selection + { + name: 'series', + type: 'relationship', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + relationTo: 'series' as any, + required: true, + label: 'Serie auswählen', + admin: { + description: 'Die anzuzeigende Serie', + }, + }, + + // Display Options + { + name: 'showHero', + type: 'checkbox', + defaultValue: true, + label: 'Hero-Bereich anzeigen', + admin: { + description: 'Cover-Bild mit Logo und Tagline', + }, + }, + { + name: 'showDescription', + type: 'checkbox', + defaultValue: true, + label: 'Beschreibung anzeigen', + }, + { + name: 'showBrandColors', + type: 'checkbox', + defaultValue: true, + label: 'Markenfarben verwenden', + admin: { + description: 'Serien-spezifische Farben für Akzente nutzen', + }, + }, + + // Related Posts + { + name: 'showRelatedPosts', + type: 'checkbox', + defaultValue: true, + label: 'Verwandte Beiträge anzeigen', + admin: { + description: 'Posts die mit dieser Serie verknüpft sind', + }, + }, + { + name: 'relatedPostsLimit', + type: 'number', + defaultValue: 6, + min: 1, + max: 20, + label: 'Anzahl verwandter Beiträge', + admin: { + condition: (_, siblingData) => siblingData?.showRelatedPosts, + }, + }, + { + name: 'relatedPostsTitle', + type: 'text', + label: 'Titel für verwandte Beiträge', + defaultValue: 'Beiträge aus dieser Serie', + localized: true, + admin: { + condition: (_, siblingData) => siblingData?.showRelatedPosts, + }, + }, + + // YouTube Integration + { + name: 'showYoutubePlaylist', + type: 'checkbox', + defaultValue: true, + label: 'YouTube-Playlist anzeigen', + admin: { + description: 'Embed der YouTube-Playlist falls konfiguriert', + }, + }, + + // Layout + { + name: 'layout', + type: 'select', + defaultValue: 'full', + label: 'Layout', + options: [ + { label: 'Vollständig', value: 'full' }, + { label: 'Kompakt', value: 'compact' }, + ], + }, + + // Hero Options (only when showHero is true) + { + name: 'hero', + type: 'group', + label: 'Hero-Optionen', + admin: { + condition: (_, siblingData) => siblingData?.showHero, + }, + fields: [ + { + name: 'height', + type: 'select', + defaultValue: 'medium', + label: 'Hero-Höhe', + options: [ + { label: 'Klein (300px)', value: 'small' }, + { label: 'Mittel (400px)', value: 'medium' }, + { label: 'Groß (500px)', value: 'large' }, + { label: 'Vollbild', value: 'fullscreen' }, + ], + }, + { + name: 'overlay', + type: 'checkbox', + defaultValue: true, + label: 'Overlay anzeigen', + admin: { + description: 'Dunkles Overlay für bessere Lesbarkeit', + }, + }, + { + name: 'textAlign', + type: 'select', + defaultValue: 'center', + label: 'Text-Ausrichtung', + options: [ + { label: 'Links', value: 'left' }, + { label: 'Mitte', value: 'center' }, + { label: 'Rechts', value: 'right' }, + ], + }, + ], + }, + ], +} diff --git a/src/blocks/VideoEmbedBlock.ts b/src/blocks/VideoEmbedBlock.ts new file mode 100644 index 0000000..4c8910a --- /dev/null +++ b/src/blocks/VideoEmbedBlock.ts @@ -0,0 +1,294 @@ +import type { Block } from 'payload' + +/** + * Aspect ratio options + */ +const aspectRatioOptions = [ + { label: '16:9 (Standard)', value: '16:9' }, + { label: '4:3', value: '4:3' }, + { label: '1:1 (Quadrat)', value: '1:1' }, + { label: '9:16 (Vertikal)', value: '9:16' }, +] + +/** + * Max width options + */ +const maxWidthOptions = [ + { label: 'Volle Breite', value: 'full' }, + { label: 'Groß (1024px)', value: 'large' }, + { label: 'Mittel (768px)', value: 'medium' }, + { label: 'Klein (512px)', value: 'small' }, +] + +/** + * VideoEmbedBlock + * + * Einbettung von YouTube, Vimeo oder benutzerdefinierten Videos. + * Unterstützt Privacy Mode, Lazy Loading und verschiedene Aspect Ratios. + * + * Verwendet die Video-Utils aus src/lib/video/video-utils.ts für URL-Parsing + */ +export const VideoEmbedBlock: Block = { + slug: 'video-embed-block', + labels: { + singular: 'Video-Embed', + plural: 'Video-Embeds', + }, + imageURL: '/assets/blocks/video-embed.png', + fields: [ + // Title (optional) + { + name: 'title', + type: 'text', + label: 'Titel', + localized: true, + admin: { + description: 'Optionaler Titel über dem Video', + }, + }, + + // Video Source Selection + { + name: 'videoSource', + type: 'select', + required: true, + defaultValue: 'youtube', + label: 'Video-Quelle', + options: [ + { label: 'YouTube', value: 'youtube' }, + { label: 'Vimeo', value: 'vimeo' }, + { label: 'Eigene URL', value: 'custom' }, + ], + }, + + // YouTube URL + { + name: 'youtubeUrl', + type: 'text', + label: 'YouTube-URL', + admin: { + description: 'z.B. https://www.youtube.com/watch?v=VIDEO_ID oder https://youtu.be/VIDEO_ID', + condition: (_, siblingData) => siblingData?.videoSource === 'youtube', + }, + validate: ( + value: string | undefined | null, + { siblingData }: { siblingData?: Record } + ) => { + if (siblingData?.videoSource !== 'youtube') return true + if (!value) return 'YouTube-URL ist erforderlich' + + // Check for valid YouTube URL patterns + const youtubePatterns = [ + /youtube\.com\/watch\?v=/, + /youtu\.be\//, + /youtube\.com\/embed\//, + /youtube\.com\/shorts\//, + /youtube-nocookie\.com\/embed\//, + ] + + const isValidYouTube = youtubePatterns.some((pattern) => pattern.test(value)) + if (!isValidYouTube) { + return 'Bitte eine gültige YouTube-URL eingeben' + } + return true + }, + }, + + // Vimeo URL + { + name: 'vimeoUrl', + type: 'text', + label: 'Vimeo-URL', + admin: { + description: 'z.B. https://vimeo.com/VIDEO_ID', + condition: (_, siblingData) => siblingData?.videoSource === 'vimeo', + }, + validate: ( + value: string | undefined | null, + { siblingData }: { siblingData?: Record } + ) => { + if (siblingData?.videoSource !== 'vimeo') return true + if (!value) return 'Vimeo-URL ist erforderlich' + + // Check for valid Vimeo URL patterns + const vimeoPatterns = [/vimeo\.com\/\d+/, /player\.vimeo\.com\/video\/\d+/] + + const isValidVimeo = vimeoPatterns.some((pattern) => pattern.test(value)) + if (!isValidVimeo) { + return 'Bitte eine gültige Vimeo-URL eingeben' + } + return true + }, + }, + + // Custom URL + { + name: 'customUrl', + type: 'text', + label: 'Video-URL', + admin: { + description: 'Direkte URL zu einer MP4, WebM oder anderen Video-Datei', + condition: (_, siblingData) => siblingData?.videoSource === 'custom', + }, + validate: ( + value: string | undefined | null, + { siblingData }: { siblingData?: Record } + ) => { + if (siblingData?.videoSource !== 'custom') return true + if (!value) return 'Video-URL ist erforderlich' + + try { + new URL(value) + return true + } catch { + return 'Bitte eine gültige URL eingeben' + } + }, + }, + + // Custom Thumbnail + { + name: 'thumbnail', + type: 'upload', + relationTo: 'media', + label: 'Vorschaubild', + admin: { + description: + 'Optional: Eigenes Vorschaubild. Bei YouTube wird automatisch das Thumbnail verwendet.', + }, + }, + + // Caption + { + name: 'caption', + type: 'text', + localized: true, + label: 'Bildunterschrift', + admin: { + description: 'Optionaler Text unter dem Video', + }, + }, + + // Privacy & Performance + { + name: 'privacyMode', + type: 'checkbox', + defaultValue: true, + label: 'Privacy Mode', + admin: { + description: 'YouTube: Verwendet youtube-nocookie.com für besseren Datenschutz', + }, + }, + { + name: 'lazyLoad', + type: 'checkbox', + defaultValue: true, + label: 'Lazy Loading', + admin: { + description: 'Video erst laden, wenn es im Viewport sichtbar ist', + }, + }, + + // Display Options + { + name: 'aspectRatio', + type: 'select', + defaultValue: '16:9', + options: aspectRatioOptions, + label: 'Seitenverhältnis', + }, + { + name: 'maxWidth', + type: 'select', + defaultValue: 'large', + options: maxWidthOptions, + label: 'Maximale Breite', + }, + + // Playback Options + { + name: 'playbackOptions', + type: 'group', + label: 'Wiedergabe-Optionen', + fields: [ + { + name: 'autoplay', + type: 'checkbox', + defaultValue: false, + label: 'Autoplay', + admin: { + description: 'Automatisch abspielen (erfordert Mute für die meisten Browser)', + }, + }, + { + name: 'muted', + type: 'checkbox', + defaultValue: false, + label: 'Stumm', + admin: { + description: 'Video stumm abspielen', + }, + }, + { + name: 'loop', + type: 'checkbox', + defaultValue: false, + label: 'Endlosschleife', + }, + { + name: 'showControls', + type: 'checkbox', + defaultValue: true, + label: 'Steuerung anzeigen', + }, + { + name: 'startTime', + type: 'number', + min: 0, + label: 'Startzeit (Sekunden)', + admin: { + description: 'Video ab dieser Sekunde starten', + }, + }, + ], + }, + + // Styling + { + name: 'style', + type: 'group', + label: 'Darstellung', + fields: [ + { + name: 'alignment', + type: 'select', + defaultValue: 'center', + label: 'Ausrichtung', + options: [ + { label: 'Links', value: 'left' }, + { label: 'Mitte', value: 'center' }, + { label: 'Rechts', value: 'right' }, + ], + }, + { + name: 'borderRadius', + type: 'select', + defaultValue: 'md', + label: 'Eckenradius', + options: [ + { label: 'Keine', value: 'none' }, + { label: 'Klein', value: 'sm' }, + { label: 'Mittel', value: 'md' }, + { label: 'Groß', value: 'lg' }, + ], + }, + { + name: 'shadow', + type: 'checkbox', + defaultValue: true, + label: 'Schatten', + }, + ], + }, + ], +} diff --git a/src/blocks/index.ts b/src/blocks/index.ts index ef420ab..44938fd 100644 --- a/src/blocks/index.ts +++ b/src/blocks/index.ts @@ -47,3 +47,10 @@ export { ComparisonBlock } from './ComparisonBlock' // Tenant-specific Blocks export { BeforeAfterBlock } from './BeforeAfterBlock' + +// BlogWoman Blocks +export { FavoritesBlock } from './FavoritesBlock' +export { SeriesBlock } from './SeriesBlock' +export { SeriesDetailBlock } from './SeriesDetailBlock' +export { VideoEmbedBlock } from './VideoEmbedBlock' +export { FeaturedContentBlock } from './FeaturedContentBlock' diff --git a/src/collections/Favorites.ts b/src/collections/Favorites.ts new file mode 100644 index 0000000..0be9d02 --- /dev/null +++ b/src/collections/Favorites.ts @@ -0,0 +1,214 @@ +// src/collections/Favorites.ts + +import type { CollectionConfig } from 'payload' +import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess' + +/** + * Category Options for Favorites + */ +const categoryOptions = [ + { label: 'Fashion', value: 'fashion' }, + { label: 'Beauty', value: 'beauty' }, + { label: 'Travel', value: 'travel' }, + { label: 'Tech', value: 'tech' }, + { label: 'Home', value: 'home' }, +] + +/** + * Badge Options for Favorites + */ +const badgeOptions = [ + { label: 'Investment Piece', value: 'investment-piece' }, + { label: 'Daily Driver', value: 'daily-driver' }, + { label: 'GRFI Approved', value: 'grfi-approved' }, + { label: 'Neu', value: 'new' }, + { label: 'Bestseller', value: 'bestseller' }, +] + +/** + * Price Range Options for Favorites + */ +const priceRangeOptions = [ + { label: 'Budget (< €50)', value: 'budget' }, + { label: 'Mid (€50-150)', value: 'mid' }, + { label: 'Premium (€150-500)', value: 'premium' }, + { label: 'Luxury (> €500)', value: 'luxury' }, +] + +/** + * Affiliate Network Options + */ +const affiliateNetworkOptions = [ + { label: 'Amazon', value: 'amazon' }, + { label: 'Awin', value: 'awin' }, + { label: 'LTK (RewardStyle)', value: 'ltk' }, + { label: 'Direct', value: 'direct' }, + { label: 'Other', value: 'other' }, +] + +/** + * Favorites Collection + * + * Affiliate-Produkte und Favoriten für BlogWoman.de + * Tenant-scoped für Multi-Tenant-Betrieb. + */ +export const Favorites: CollectionConfig = { + slug: 'favorites', + admin: { + group: 'Content', + useAsTitle: 'title', + defaultColumns: ['title', 'category', 'featured', 'isActive'], + description: 'Affiliate-Produkte und Favoriten', + }, + access: { + read: tenantScopedPublicRead, + create: authenticatedOnly, + update: authenticatedOnly, + delete: authenticatedOnly, + }, + fields: [ + // Main Content + { + name: 'title', + type: 'text', + required: true, + maxLength: 200, + label: 'Titel', + }, + { + name: 'slug', + type: 'text', + required: true, + unique: true, + label: 'Slug', + admin: { + position: 'sidebar', + description: 'URL-freundlicher Name', + }, + }, + { + name: 'description', + type: 'textarea', + maxLength: 300, + label: 'Beschreibung', + admin: { + description: 'Kurze Produktbeschreibung (max. 300 Zeichen)', + }, + }, + + // Categorization + { + name: 'category', + type: 'select', + required: true, + options: categoryOptions, + label: 'Kategorie', + }, + { + name: 'subcategory', + type: 'text', + maxLength: 100, + label: 'Unterkategorie', + admin: { + description: 'z.B. "Taschen", "Hautpflege", "Laptops"', + }, + }, + + // Pricing + { + name: 'price', + type: 'number', + min: 0, + label: 'Preis (€)', + admin: { + description: 'Aktueller Preis in Euro', + }, + }, + { + name: 'priceRange', + type: 'select', + options: priceRangeOptions, + label: 'Preiskategorie', + }, + + // Affiliate + { + name: 'affiliateUrl', + type: 'text', + required: true, + label: 'Affiliate-URL', + admin: { + description: 'Vollständiger Affiliate-Link mit Tracking', + }, + validate: (value: string | undefined | null) => { + if (!value) return 'Affiliate-URL ist erforderlich' + try { + new URL(value) + return true + } catch { + return 'Bitte eine gültige URL eingeben' + } + }, + }, + { + name: 'affiliateNetwork', + type: 'select', + options: affiliateNetworkOptions, + label: 'Affiliate-Netzwerk', + }, + + // Media + { + name: 'image', + type: 'upload', + relationTo: 'media', + required: true, + label: 'Produktbild', + admin: { + description: 'Hauptbild des Produkts', + }, + }, + + // Display Options + { + name: 'badge', + type: 'select', + options: badgeOptions, + label: 'Badge', + admin: { + description: 'Optionales Label/Abzeichen', + }, + }, + { + name: 'featured', + type: 'checkbox', + defaultValue: false, + label: 'Featured', + admin: { + position: 'sidebar', + description: 'In der Featured-Sektion anzeigen', + }, + }, + { + name: 'isActive', + type: 'checkbox', + required: true, + defaultValue: true, + label: 'Aktiv', + admin: { + position: 'sidebar', + description: 'Inaktive Favoriten werden nicht angezeigt', + }, + }, + { + name: 'order', + type: 'number', + defaultValue: 0, + label: 'Sortierung', + admin: { + position: 'sidebar', + description: 'Niedrigere Zahlen werden zuerst angezeigt', + }, + }, + ], +} diff --git a/src/collections/Pages.ts b/src/collections/Pages.ts index 4944448..d7afe97 100644 --- a/src/collections/Pages.ts +++ b/src/collections/Pages.ts @@ -43,6 +43,12 @@ import { ComparisonBlock, // Tenant-specific Blocks BeforeAfterBlock, + // BlogWoman Blocks + FavoritesBlock, + SeriesBlock, + SeriesDetailBlock, + VideoEmbedBlock, + FeaturedContentBlock, } from '../blocks' import { pagesAccess } from '../lib/access' @@ -140,6 +146,12 @@ export const Pages: CollectionConfig = { ComparisonBlock, // Tenant-specific Blocks BeforeAfterBlock, + // BlogWoman Blocks + FavoritesBlock, + SeriesBlock, + SeriesDetailBlock, + VideoEmbedBlock, + FeaturedContentBlock, ], }, { diff --git a/src/collections/Series.ts b/src/collections/Series.ts new file mode 100644 index 0000000..dabd87e --- /dev/null +++ b/src/collections/Series.ts @@ -0,0 +1,173 @@ +// src/collections/Series.ts + +import type { CollectionConfig } from 'payload' +import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess' + +/** + * Series Collection + * + * YouTube-Serien mit eigenem Branding für BlogWoman.de + * Unterstützt lokalisierte Inhalte und Custom Brand Colors. + * Tenant-scoped für Multi-Tenant-Betrieb. + */ +export const Series: CollectionConfig = { + slug: 'series', + admin: { + group: 'Content', + useAsTitle: 'title', + defaultColumns: ['title', 'slug', 'brandColor', 'isActive'], + description: 'YouTube-Serien und Content-Reihen', + }, + access: { + read: tenantScopedPublicRead, + create: authenticatedOnly, + update: authenticatedOnly, + delete: authenticatedOnly, + }, + fields: [ + // Main Content + { + name: 'title', + type: 'text', + required: true, + localized: true, + label: 'Titel', + }, + { + name: 'slug', + type: 'text', + required: true, + unique: true, + label: 'Slug', + admin: { + description: 'URL-freundlicher Name (z.B. "grfi-series")', + }, + }, + { + name: 'tagline', + type: 'text', + maxLength: 150, + localized: true, + label: 'Tagline', + admin: { + description: 'Kurzer Slogan oder Untertitel (max. 150 Zeichen)', + }, + }, + { + name: 'description', + type: 'richText', + localized: true, + label: 'Beschreibung', + admin: { + description: 'Ausführliche Beschreibung der Serie', + }, + }, + + // Branding + { + name: 'logo', + type: 'upload', + relationTo: 'media', + label: 'Logo', + admin: { + description: 'Serien-Logo (empfohlen: transparent PNG)', + }, + }, + { + name: 'coverImage', + type: 'upload', + relationTo: 'media', + label: 'Cover-Bild', + admin: { + description: 'Hauptbild für die Serie (16:9 empfohlen)', + }, + }, + { + name: 'brandColor', + type: 'text', + label: 'Markenfarbe', + admin: { + description: 'Hex-Farbcode (z.B. #B08D57)', + placeholder: '#B08D57', + }, + validate: (value: string | undefined | null) => { + if (!value) return true // Optional field + const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ + if (!hexRegex.test(value)) { + return 'Bitte einen gültigen Hex-Farbcode eingeben (z.B. #B08D57)' + } + return true + }, + }, + { + name: 'accentColor', + type: 'text', + label: 'Akzentfarbe', + admin: { + description: 'Sekundäre Farbe (z.B. #FFFFFF)', + placeholder: '#FFFFFF', + }, + validate: (value: string | undefined | null) => { + if (!value) return true // Optional field + const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ + if (!hexRegex.test(value)) { + return 'Bitte einen gültigen Hex-Farbcode eingeben (z.B. #FFFFFF)' + } + return true + }, + }, + + // YouTube Integration + { + name: 'youtubePlaylistId', + type: 'text', + label: 'YouTube Playlist ID', + admin: { + description: 'Die Playlist-ID aus YouTube (z.B. PLxxxxxx)', + }, + }, + { + name: 'youtubePlaylistUrl', + type: 'text', + label: 'YouTube Playlist URL', + admin: { + description: 'Vollständiger Link zur YouTube-Playlist', + }, + validate: (value: string | undefined | null) => { + if (!value) return true // Optional field + try { + const url = new URL(value) + if (!url.hostname.includes('youtube.com') && !url.hostname.includes('youtu.be')) { + return 'Bitte eine gültige YouTube-URL eingeben' + } + return true + } catch { + return 'Bitte eine gültige URL eingeben' + } + }, + }, + + // Display Settings + { + name: 'order', + type: 'number', + defaultValue: 0, + label: 'Sortierung', + admin: { + position: 'sidebar', + description: 'Niedrigere Zahlen werden zuerst angezeigt', + }, + }, + { + name: 'isActive', + type: 'checkbox', + required: true, + defaultValue: true, + label: 'Aktiv', + admin: { + position: 'sidebar', + description: 'Inaktive Serien werden nicht angezeigt', + }, + }, + ], +} diff --git a/src/payload.config.ts b/src/payload.config.ts index 03c01e9..ca4a8ab 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -64,6 +64,10 @@ import { Bookings } from './collections/Bookings' import { Certifications } from './collections/Certifications' import { Projects } from './collections/Projects' +// BlogWoman Collections +import { Favorites } from './collections/Favorites' +import { Series } from './collections/Series' + // Consent Management Collections import { CookieConfigurations } from './collections/CookieConfigurations' import { CookieInventory } from './collections/CookieInventory' @@ -197,6 +201,9 @@ export default buildConfig({ Bookings, Certifications, Projects, + // BlogWoman Collections + Favorites, + Series, // Consent Management CookieConfigurations, CookieInventory, @@ -266,6 +273,9 @@ export default buildConfig({ bookings: {}, certifications: {}, projects: {}, + // BlogWoman Collections + favorites: {}, + series: {}, // Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben 'cookie-configurations': { customTenantField: true }, 'cookie-inventory': { customTenantField: true },