From 95c9d2a4bc67c0a987bd5fb2443eb873459b7cd9 Mon Sep 17 00:00:00 2001 From: Martin Porwoll Date: Mon, 1 Dec 2025 08:19:15 +0000 Subject: [PATCH] feat: add content blocks and global settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocks for page builder: - HeroBlock: hero sections with CTA - TextBlock: rich text content - ImageTextBlock: image with text layout - CardGridBlock: grid of cards - CTABlock: call-to-action sections - QuoteBlock: testimonial quotes - VideoBlock: embedded videos - DividerBlock: visual separators - ContactFormBlock: contact forms - NewsletterBlock: newsletter signup - ProcessStepsBlock: step-by-step processes - TimelineBlock: timeline displays - TestimonialsBlock: testimonial carousels - PostsListBlock: blog post listings Globals: - Navigation: site navigation structure - SiteSettings: general site configuration - SEOSettings: default SEO settings per tenant 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/blocks/CTABlock.ts | 67 ++++++ src/blocks/CardGridBlock.ts | 68 ++++++ src/blocks/ContactFormBlock.ts | 48 ++++ src/blocks/DividerBlock.ts | 33 +++ src/blocks/HeroBlock.ts | 76 +++++++ src/blocks/ImageTextBlock.ts | 58 +++++ src/blocks/NewsletterBlock.ts | 156 +++++++++++++ src/blocks/PostsListBlock.ts | 159 +++++++++++++ src/blocks/ProcessStepsBlock.ts | 145 ++++++++++++ src/blocks/QuoteBlock.ts | 47 ++++ src/blocks/TestimonialsBlock.ts | 145 ++++++++++++ src/blocks/TextBlock.ts | 29 +++ src/blocks/TimelineBlock.ts | 128 +++++++++++ src/blocks/VideoBlock.ts | 37 +++ src/blocks/index.ts | 16 ++ src/globals/Navigation.ts | 131 +++++++++++ src/globals/SEOSettings.ts | 385 ++++++++++++++++++++++++++++++++ src/globals/SiteSettings.ts | 92 ++++++++ 18 files changed, 1820 insertions(+) create mode 100644 src/blocks/CTABlock.ts create mode 100644 src/blocks/CardGridBlock.ts create mode 100644 src/blocks/ContactFormBlock.ts create mode 100644 src/blocks/DividerBlock.ts create mode 100644 src/blocks/HeroBlock.ts create mode 100644 src/blocks/ImageTextBlock.ts create mode 100644 src/blocks/NewsletterBlock.ts create mode 100644 src/blocks/PostsListBlock.ts create mode 100644 src/blocks/ProcessStepsBlock.ts create mode 100644 src/blocks/QuoteBlock.ts create mode 100644 src/blocks/TestimonialsBlock.ts create mode 100644 src/blocks/TextBlock.ts create mode 100644 src/blocks/TimelineBlock.ts create mode 100644 src/blocks/VideoBlock.ts create mode 100644 src/blocks/index.ts create mode 100644 src/globals/Navigation.ts create mode 100644 src/globals/SEOSettings.ts create mode 100644 src/globals/SiteSettings.ts diff --git a/src/blocks/CTABlock.ts b/src/blocks/CTABlock.ts new file mode 100644 index 0000000..abd33cf --- /dev/null +++ b/src/blocks/CTABlock.ts @@ -0,0 +1,67 @@ +import type { Block } from 'payload' + +export const CTABlock: Block = { + slug: 'cta-block', + labels: { + singular: 'Call-to-Action', + plural: 'Call-to-Actions', + }, + fields: [ + { + name: 'headline', + type: 'text', + required: true, + label: 'Überschrift', + localized: true, + }, + { + name: 'description', + type: 'textarea', + label: 'Beschreibung', + localized: true, + }, + { + name: 'buttons', + type: 'array', + label: 'Buttons', + maxRows: 2, + fields: [ + { + name: 'text', + type: 'text', + required: true, + label: 'Button-Text', + localized: true, + }, + { + name: 'link', + type: 'text', + required: true, + label: 'Link', + }, + { + name: 'style', + type: 'select', + defaultValue: 'primary', + label: 'Stil', + options: [ + { label: 'Primär', value: 'primary' }, + { label: 'Sekundär', value: 'secondary' }, + { label: 'Outline', value: 'outline' }, + ], + }, + ], + }, + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'dark', + label: 'Hintergrundfarbe', + options: [ + { label: 'Dunkel', value: 'dark' }, + { label: 'Hell', value: 'light' }, + { label: 'Akzent', value: 'accent' }, + ], + }, + ], +} diff --git a/src/blocks/CardGridBlock.ts b/src/blocks/CardGridBlock.ts new file mode 100644 index 0000000..b84ad82 --- /dev/null +++ b/src/blocks/CardGridBlock.ts @@ -0,0 +1,68 @@ +import type { Block } from 'payload' + +export const CardGridBlock: Block = { + slug: 'card-grid-block', + labels: { + singular: 'Karten-Raster', + plural: 'Karten-Raster', + }, + fields: [ + { + name: 'headline', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'cards', + type: 'array', + label: 'Karten', + minRows: 1, + maxRows: 6, + fields: [ + { + name: 'image', + type: 'upload', + relationTo: 'media', + label: 'Bild', + }, + { + name: 'title', + type: 'text', + required: true, + label: 'Titel', + localized: true, + }, + { + name: 'description', + type: 'textarea', + label: 'Beschreibung', + localized: true, + }, + { + name: 'link', + type: 'text', + label: 'Link', + }, + { + name: 'linkText', + type: 'text', + defaultValue: 'mehr', + label: 'Link-Text', + localized: true, + }, + ], + }, + { + name: 'columns', + type: 'select', + defaultValue: '3', + label: 'Spalten', + options: [ + { label: '2 Spalten', value: '2' }, + { label: '3 Spalten', value: '3' }, + { label: '4 Spalten', value: '4' }, + ], + }, + ], +} diff --git a/src/blocks/ContactFormBlock.ts b/src/blocks/ContactFormBlock.ts new file mode 100644 index 0000000..da0ab33 --- /dev/null +++ b/src/blocks/ContactFormBlock.ts @@ -0,0 +1,48 @@ +import type { Block } from 'payload' + +export const ContactFormBlock: Block = { + slug: 'contact-form-block', + labels: { + singular: 'Kontaktformular', + plural: 'Kontaktformulare', + }, + fields: [ + { + name: 'headline', + type: 'text', + defaultValue: 'Kontakt', + label: 'Überschrift', + localized: true, + }, + { + name: 'description', + type: 'textarea', + label: 'Beschreibung', + localized: true, + }, + { + name: 'recipientEmail', + type: 'email', + defaultValue: 'info@porwoll.de', + label: 'Empfänger E-Mail', + }, + { + name: 'showPhone', + type: 'checkbox', + defaultValue: true, + label: 'Telefon anzeigen', + }, + { + name: 'showAddress', + type: 'checkbox', + defaultValue: true, + label: 'Adresse anzeigen', + }, + { + name: 'showSocials', + type: 'checkbox', + defaultValue: true, + label: 'Social Media anzeigen', + }, + ], +} diff --git a/src/blocks/DividerBlock.ts b/src/blocks/DividerBlock.ts new file mode 100644 index 0000000..bfb7c7b --- /dev/null +++ b/src/blocks/DividerBlock.ts @@ -0,0 +1,33 @@ +import type { Block } from 'payload' + +export const DividerBlock: Block = { + slug: 'divider-block', + labels: { + singular: 'Trenner', + plural: 'Trenner', + }, + fields: [ + { + name: 'style', + type: 'select', + defaultValue: 'space', + label: 'Stil', + options: [ + { label: 'Linie', value: 'line' }, + { label: 'Abstand', value: 'space' }, + { label: 'Punkte', value: 'dots' }, + ], + }, + { + name: 'spacing', + type: 'select', + defaultValue: 'medium', + label: 'Abstand', + options: [ + { label: 'Klein', value: 'small' }, + { label: 'Mittel', value: 'medium' }, + { label: 'Groß', value: 'large' }, + ], + }, + ], +} diff --git a/src/blocks/HeroBlock.ts b/src/blocks/HeroBlock.ts new file mode 100644 index 0000000..3a1b802 --- /dev/null +++ b/src/blocks/HeroBlock.ts @@ -0,0 +1,76 @@ +import type { Block } from 'payload' + +export const HeroBlock: Block = { + slug: 'hero-block', + labels: { + singular: 'Hero', + plural: 'Heroes', + }, + fields: [ + { + name: 'backgroundImage', + type: 'upload', + relationTo: 'media', + label: 'Hintergrundbild', + }, + { + name: 'headline', + type: 'text', + required: true, + label: 'Überschrift', + localized: true, + }, + { + name: 'subline', + type: 'textarea', + label: 'Unterüberschrift', + localized: true, + }, + { + name: 'alignment', + type: 'select', + defaultValue: 'center', + label: 'Ausrichtung', + options: [ + { label: 'Links', value: 'left' }, + { label: 'Zentriert', value: 'center' }, + { label: 'Rechts', value: 'right' }, + ], + }, + { + name: 'overlay', + type: 'checkbox', + defaultValue: true, + label: 'Dunkles Overlay', + }, + { + name: 'cta', + type: 'group', + label: 'Call-to-Action', + fields: [ + { + name: 'text', + type: 'text', + label: 'Button-Text', + localized: true, + }, + { + name: 'link', + type: 'text', + label: 'Link', + }, + { + name: 'style', + type: 'select', + defaultValue: 'primary', + label: 'Button-Stil', + options: [ + { label: 'Primär', value: 'primary' }, + { label: 'Sekundär', value: 'secondary' }, + { label: 'Outline', value: 'outline' }, + ], + }, + ], + }, + ], +} diff --git a/src/blocks/ImageTextBlock.ts b/src/blocks/ImageTextBlock.ts new file mode 100644 index 0000000..72459b1 --- /dev/null +++ b/src/blocks/ImageTextBlock.ts @@ -0,0 +1,58 @@ +import type { Block } from 'payload' + +export const ImageTextBlock: Block = { + slug: 'image-text-block', + labels: { + singular: 'Bild & Text', + plural: 'Bild & Text', + }, + fields: [ + { + name: 'image', + type: 'upload', + relationTo: 'media', + required: true, + label: 'Bild', + }, + { + name: 'imagePosition', + type: 'select', + defaultValue: 'left', + label: 'Bildposition', + options: [ + { label: 'Links', value: 'left' }, + { label: 'Rechts', value: 'right' }, + ], + }, + { + name: 'headline', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'content', + type: 'richText', + label: 'Inhalt', + localized: true, + }, + { + name: 'cta', + type: 'group', + label: 'Call-to-Action', + fields: [ + { + name: 'text', + type: 'text', + label: 'Button-Text', + localized: true, + }, + { + name: 'link', + type: 'text', + label: 'Link', + }, + ], + }, + ], +} diff --git a/src/blocks/NewsletterBlock.ts b/src/blocks/NewsletterBlock.ts new file mode 100644 index 0000000..562a4f5 --- /dev/null +++ b/src/blocks/NewsletterBlock.ts @@ -0,0 +1,156 @@ +import type { Block } from 'payload' + +/** + * Newsletter Block + * Anmeldeformular für Newsletter + */ +export const NewsletterBlock: Block = { + slug: 'newsletter-block', + labels: { + singular: 'Newsletter Anmeldung', + plural: 'Newsletter Anmeldungen', + }, + fields: [ + { + name: 'title', + type: 'text', + defaultValue: 'Newsletter abonnieren', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'textarea', + defaultValue: 'Erhalten Sie regelmäßig Updates und Neuigkeiten direkt in Ihr Postfach.', + label: 'Beschreibung', + localized: true, + }, + { + name: 'layout', + type: 'select', + defaultValue: 'inline', + label: 'Layout', + options: [ + { label: 'Inline (Eingabe + Button nebeneinander)', value: 'inline' }, + { label: 'Gestapelt (untereinander)', value: 'stacked' }, + { label: 'Mit Bild (50/50)', value: 'with-image' }, + { label: 'Minimal (nur Input)', value: 'minimal' }, + { label: 'Card (Karte)', value: 'card' }, + ], + }, + { + name: 'image', + type: 'upload', + relationTo: 'media', + label: 'Bild', + admin: { + condition: (data, siblingData) => siblingData?.layout === 'with-image', + }, + }, + { + name: 'imagePosition', + type: 'select', + defaultValue: 'left', + label: 'Bildposition', + options: [ + { label: 'Links', value: 'left' }, + { label: 'Rechts', value: 'right' }, + ], + admin: { + condition: (data, siblingData) => siblingData?.layout === 'with-image', + }, + }, + { + name: 'collectName', + type: 'checkbox', + defaultValue: false, + label: 'Name abfragen', + }, + { + name: 'showInterests', + type: 'checkbox', + defaultValue: false, + label: 'Interessen zur Auswahl anbieten', + }, + { + name: 'availableInterests', + type: 'select', + hasMany: true, + label: 'Verfügbare Interessen', + options: [ + { label: 'Allgemeine Updates', value: 'general' }, + { label: 'Blog-Artikel', value: 'blog' }, + { label: 'Produkt-News', value: 'products' }, + { label: 'Angebote & Aktionen', value: 'offers' }, + { label: 'Events', value: 'events' }, + ], + admin: { + condition: (data, siblingData) => siblingData?.showInterests, + }, + }, + { + name: 'buttonText', + type: 'text', + defaultValue: 'Anmelden', + label: 'Button-Text', + localized: true, + }, + { + name: 'placeholderEmail', + type: 'text', + defaultValue: 'Ihre E-Mail-Adresse', + label: 'Placeholder E-Mail', + localized: true, + }, + { + name: 'successMessage', + type: 'textarea', + defaultValue: + 'Vielen Dank! Bitte bestätigen Sie Ihre E-Mail-Adresse über den Link in der Bestätigungsmail.', + label: 'Erfolgsmeldung', + localized: true, + }, + { + name: 'errorMessage', + type: 'text', + defaultValue: 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.', + label: 'Fehlermeldung', + localized: true, + }, + { + name: 'privacyText', + type: 'textarea', + defaultValue: + 'Mit der Anmeldung akzeptieren Sie unsere Datenschutzerklärung. Sie können sich jederzeit abmelden.', + label: 'Datenschutz-Hinweis', + localized: true, + }, + { + name: 'privacyLink', + type: 'text', + defaultValue: '/datenschutz', + label: 'Link zur Datenschutzerklärung', + }, + { + name: 'source', + type: 'text', + defaultValue: 'website', + label: 'Tracking-Quelle', + admin: { + description: 'Wird gespeichert um zu tracken, wo die Anmeldung erfolgte', + }, + }, + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'accent', + label: 'Hintergrund', + options: [ + { label: 'Weiß', value: 'white' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, + { label: 'Akzentfarbe', value: 'accent' }, + ], + }, + ], +} diff --git a/src/blocks/PostsListBlock.ts b/src/blocks/PostsListBlock.ts new file mode 100644 index 0000000..f960b6b --- /dev/null +++ b/src/blocks/PostsListBlock.ts @@ -0,0 +1,159 @@ +import type { Block } from 'payload' + +/** + * Posts List Block + * Zeigt Blog-Artikel, News oder andere Post-Typen an + */ +export const PostsListBlock: Block = { + slug: 'posts-list-block', + labels: { + singular: 'Blog/News Liste', + plural: 'Blog/News Listen', + }, + fields: [ + { + name: 'title', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + { + name: 'postType', + type: 'select', + required: true, + defaultValue: 'blog', + label: 'Beitragstyp', + options: [ + { label: 'Blog-Artikel', value: 'blog' }, + { label: 'News/Aktuelles', value: 'news' }, + { label: 'Pressemitteilungen', value: 'press' }, + { label: 'Ankündigungen', value: 'announcement' }, + { label: 'Alle Beiträge', value: 'all' }, + ], + }, + { + name: 'layout', + type: 'select', + defaultValue: 'grid', + label: 'Layout', + options: [ + { label: 'Grid (Karten)', value: 'grid' }, + { label: 'Liste', value: 'list' }, + { label: 'Featured + Grid', value: 'featured' }, + { label: 'Kompakt (Sidebar)', value: 'compact' }, + { label: 'Masonry', value: 'masonry' }, + ], + }, + { + 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: (data, siblingData) => + siblingData?.layout === 'grid' || + siblingData?.layout === 'featured' || + siblingData?.layout === 'masonry', + }, + }, + { + name: 'limit', + type: 'number', + defaultValue: 6, + min: 1, + max: 24, + label: 'Anzahl Beiträge', + }, + { + name: 'showFeaturedOnly', + type: 'checkbox', + defaultValue: false, + label: 'Nur hervorgehobene anzeigen', + }, + { + name: 'filterByCategory', + type: 'relationship', + relationTo: 'categories', + hasMany: true, + label: 'Nach Kategorien filtern', + admin: { + description: 'Leer = alle Kategorien', + }, + }, + { + name: 'showExcerpt', + type: 'checkbox', + defaultValue: true, + label: 'Kurzfassung anzeigen', + }, + { + name: 'showDate', + type: 'checkbox', + defaultValue: true, + label: 'Datum anzeigen', + }, + { + name: 'showAuthor', + type: 'checkbox', + defaultValue: false, + label: 'Autor anzeigen', + }, + { + name: 'showCategory', + type: 'checkbox', + defaultValue: true, + label: 'Kategorie anzeigen', + }, + { + name: 'showPagination', + type: 'checkbox', + defaultValue: false, + label: 'Pagination anzeigen', + }, + { + name: 'showReadMore', + type: 'checkbox', + defaultValue: true, + label: '"Alle anzeigen" Link', + }, + { + name: 'readMoreLabel', + type: 'text', + defaultValue: 'Alle Beiträge anzeigen', + localized: true, + admin: { + condition: (data, siblingData) => siblingData?.showReadMore, + }, + }, + { + name: 'readMoreLink', + type: 'text', + defaultValue: '/blog', + admin: { + condition: (data, siblingData) => siblingData?.showReadMore, + }, + }, + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'white', + label: 'Hintergrund', + options: [ + { label: 'Weiß', value: 'white' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, + ], + }, + ], +} diff --git a/src/blocks/ProcessStepsBlock.ts b/src/blocks/ProcessStepsBlock.ts new file mode 100644 index 0000000..3f98c24 --- /dev/null +++ b/src/blocks/ProcessStepsBlock.ts @@ -0,0 +1,145 @@ +import type { Block } from 'payload' + +/** + * Process Steps Block + * Zeigt Prozess-Schritte / "So funktioniert es" + */ +export const ProcessStepsBlock: Block = { + slug: 'process-steps-block', + labels: { + singular: 'Prozess/Schritte', + plural: 'Prozess/Schritte', + }, + fields: [ + { + name: 'title', + type: 'text', + defaultValue: 'So funktioniert es', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + { + name: 'layout', + type: 'select', + defaultValue: 'horizontal', + label: 'Layout', + options: [ + { label: 'Horizontal (nebeneinander)', value: 'horizontal' }, + { label: 'Vertikal (untereinander)', value: 'vertical' }, + { label: 'Alternierend (Zickzack)', value: 'alternating' }, + { label: 'Mit Verbindungslinien', value: 'connected' }, + { label: 'Timeline-Stil', value: 'timeline' }, + ], + }, + { + name: 'showNumbers', + type: 'checkbox', + defaultValue: true, + label: 'Schritt-Nummern anzeigen', + }, + { + name: 'showIcons', + type: 'checkbox', + defaultValue: true, + label: 'Icons anzeigen', + }, + { + name: 'steps', + type: 'array', + label: 'Schritte', + minRows: 2, + maxRows: 10, + fields: [ + { + name: 'title', + type: 'text', + required: true, + label: 'Schritt-Titel', + localized: true, + }, + { + name: 'description', + type: 'textarea', + label: 'Beschreibung', + localized: true, + }, + { + name: 'icon', + type: 'text', + label: 'Icon', + admin: { + description: 'Emoji oder Icon-Name (z.B. "📞", "✓", "1")', + }, + }, + { + name: 'image', + type: 'upload', + relationTo: 'media', + label: 'Bild (optional)', + }, + ], + }, + { + name: 'cta', + type: 'group', + label: 'Call-to-Action', + fields: [ + { + name: 'show', + type: 'checkbox', + defaultValue: false, + label: 'CTA anzeigen', + }, + { + name: 'label', + type: 'text', + defaultValue: 'Jetzt starten', + label: 'Button-Text', + localized: true, + admin: { + condition: (data, siblingData) => siblingData?.show, + }, + }, + { + name: 'href', + type: 'text', + label: 'Link', + admin: { + condition: (data, siblingData) => siblingData?.show, + }, + }, + { + name: 'variant', + type: 'select', + defaultValue: 'default', + label: 'Button-Stil', + options: [ + { label: 'Standard', value: 'default' }, + { label: 'Ghost', value: 'ghost' }, + { label: 'Light', value: 'light' }, + ], + admin: { + condition: (data, siblingData) => siblingData?.show, + }, + }, + ], + }, + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'white', + label: 'Hintergrund', + options: [ + { label: 'Weiß', value: 'white' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, + ], + }, + ], +} diff --git a/src/blocks/QuoteBlock.ts b/src/blocks/QuoteBlock.ts new file mode 100644 index 0000000..268667b --- /dev/null +++ b/src/blocks/QuoteBlock.ts @@ -0,0 +1,47 @@ +import type { Block } from 'payload' + +export const QuoteBlock: Block = { + slug: 'quote-block', + labels: { + singular: 'Zitat', + plural: 'Zitate', + }, + fields: [ + { + name: 'quote', + type: 'textarea', + required: true, + label: 'Zitat', + localized: true, + }, + { + name: 'author', + type: 'text', + label: 'Autor', + // Author bleibt nicht lokalisiert - Namen sind sprachunabhängig + }, + { + name: 'role', + type: 'text', + label: 'Rolle/Position', + localized: true, + }, + { + name: 'image', + type: 'upload', + relationTo: 'media', + label: 'Autorenbild', + }, + { + name: 'style', + type: 'select', + defaultValue: 'simple', + label: 'Stil', + options: [ + { label: 'Einfach', value: 'simple' }, + { label: 'Hervorgehoben', value: 'highlighted' }, + { label: 'Mit Bild', value: 'with-image' }, + ], + }, + ], +} diff --git a/src/blocks/TestimonialsBlock.ts b/src/blocks/TestimonialsBlock.ts new file mode 100644 index 0000000..3413a6a --- /dev/null +++ b/src/blocks/TestimonialsBlock.ts @@ -0,0 +1,145 @@ +import type { Block } from 'payload' + +/** + * Testimonials Block + * Zeigt Kundenstimmen aus der Testimonials Collection + */ +export const TestimonialsBlock: Block = { + slug: 'testimonials-block', + labels: { + singular: 'Testimonials', + plural: 'Testimonials', + }, + fields: [ + { + name: 'title', + type: 'text', + defaultValue: 'Das sagen unsere Kunden', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + { + name: 'layout', + type: 'select', + defaultValue: 'slider', + label: 'Layout', + options: [ + { label: 'Slider/Karussell', value: 'slider' }, + { label: 'Grid (Karten)', value: 'grid' }, + { label: 'Einzeln (Featured)', value: 'single' }, + { label: 'Masonry', value: 'masonry' }, + { label: 'Liste', value: 'list' }, + ], + }, + { + 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: (data, siblingData) => + siblingData?.layout === 'grid' || siblingData?.layout === 'masonry', + }, + }, + { + name: 'displayMode', + type: 'select', + defaultValue: 'all', + label: 'Auswahl', + options: [ + { label: 'Alle aktiven Testimonials', value: 'all' }, + { label: 'Handverlesene Auswahl', value: 'selected' }, + ], + }, + { + name: 'selectedTestimonials', + type: 'relationship', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + relationTo: 'testimonials' as any, + hasMany: true, + label: 'Testimonials auswählen', + admin: { + condition: (data, siblingData) => siblingData?.displayMode === 'selected', + }, + }, + { + name: 'limit', + type: 'number', + defaultValue: 6, + min: 1, + max: 20, + label: 'Maximale Anzahl', + admin: { + condition: (data, siblingData) => siblingData?.displayMode === 'all', + }, + }, + { + name: 'showRating', + type: 'checkbox', + defaultValue: true, + label: 'Sterne-Bewertung anzeigen', + }, + { + name: 'showImage', + type: 'checkbox', + defaultValue: true, + label: 'Foto anzeigen', + }, + { + name: 'showCompany', + type: 'checkbox', + defaultValue: true, + label: 'Unternehmen anzeigen', + }, + { + name: 'showSource', + type: 'checkbox', + defaultValue: false, + label: 'Quelle anzeigen', + }, + { + name: 'autoplay', + type: 'checkbox', + defaultValue: true, + label: 'Automatisch wechseln', + admin: { + condition: (data, siblingData) => siblingData?.layout === 'slider', + }, + }, + { + name: 'autoplaySpeed', + type: 'number', + defaultValue: 5000, + min: 2000, + max: 15000, + label: 'Wechselintervall (ms)', + admin: { + condition: (data, siblingData) => + siblingData?.layout === 'slider' && siblingData?.autoplay, + }, + }, + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'light', + label: 'Hintergrund', + options: [ + { label: 'Weiß', value: 'white' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, + { label: 'Akzentfarbe', value: 'accent' }, + ], + }, + ], +} diff --git a/src/blocks/TextBlock.ts b/src/blocks/TextBlock.ts new file mode 100644 index 0000000..eca78c9 --- /dev/null +++ b/src/blocks/TextBlock.ts @@ -0,0 +1,29 @@ +import type { Block } from 'payload' + +export const TextBlock: Block = { + slug: 'text-block', + labels: { + singular: 'Text', + plural: 'Texte', + }, + fields: [ + { + name: 'content', + type: 'richText', + required: true, + label: 'Inhalt', + localized: true, + }, + { + name: 'width', + type: 'select', + defaultValue: 'medium', + label: 'Breite', + options: [ + { label: 'Schmal', value: 'narrow' }, + { label: 'Mittel', value: 'medium' }, + { label: 'Volle Breite', value: 'full' }, + ], + }, + ], +} diff --git a/src/blocks/TimelineBlock.ts b/src/blocks/TimelineBlock.ts new file mode 100644 index 0000000..355d750 --- /dev/null +++ b/src/blocks/TimelineBlock.ts @@ -0,0 +1,128 @@ +import type { Block } from 'payload' + +/** + * Timeline Block (erweitert) + * Chronologische Darstellung von Ereignissen + */ +export const TimelineBlock: Block = { + slug: 'timeline-block', + labels: { + singular: 'Timeline', + plural: 'Timelines', + }, + fields: [ + { + name: 'title', + type: 'text', + label: 'Überschrift', + localized: true, + }, + { + name: 'subtitle', + type: 'text', + label: 'Untertitel', + localized: true, + }, + { + name: 'layout', + type: 'select', + defaultValue: 'vertical', + label: 'Layout', + options: [ + { label: 'Vertikal (Standard)', value: 'vertical' }, + { label: 'Alternierend (links/rechts)', value: 'alternating' }, + { label: 'Horizontal (Zeitleiste)', value: 'horizontal' }, + ], + }, + { + name: 'showConnector', + type: 'checkbox', + defaultValue: true, + label: 'Verbindungslinie anzeigen', + }, + { + name: 'markerStyle', + type: 'select', + defaultValue: 'dot', + label: 'Marker-Stil', + options: [ + { label: 'Punkt', value: 'dot' }, + { label: 'Nummer', value: 'number' }, + { label: 'Icon', value: 'icon' }, + { label: 'Jahr/Datum', value: 'date' }, + ], + }, + { + name: 'items', + type: 'array', + label: 'Einträge', + minRows: 1, + fields: [ + { + name: 'year', + type: 'text', + label: 'Jahr/Datum', + admin: { + description: 'z.B. "2024", "Januar 2024", "15.03.2024"', + }, + }, + { + name: 'title', + type: 'text', + required: true, + label: 'Titel', + localized: true, + }, + { + name: 'description', + type: 'textarea', + label: 'Beschreibung', + localized: true, + }, + { + name: 'icon', + type: 'text', + label: 'Icon', + admin: { + description: 'Emoji oder Icon-Name', + }, + }, + { + name: 'image', + type: 'upload', + relationTo: 'media', + label: 'Bild (optional)', + }, + { + name: 'link', + type: 'group', + label: 'Link (optional)', + fields: [ + { + name: 'label', + type: 'text', + label: 'Link-Text', + localized: true, + }, + { + name: 'href', + type: 'text', + label: 'URL', + }, + ], + }, + ], + }, + { + name: 'backgroundColor', + type: 'select', + defaultValue: 'white', + label: 'Hintergrund', + options: [ + { label: 'Weiß', value: 'white' }, + { label: 'Hell (Grau)', value: 'light' }, + { label: 'Dunkel', value: 'dark' }, + ], + }, + ], +} diff --git a/src/blocks/VideoBlock.ts b/src/blocks/VideoBlock.ts new file mode 100644 index 0000000..8af9f7e --- /dev/null +++ b/src/blocks/VideoBlock.ts @@ -0,0 +1,37 @@ +import type { Block } from 'payload' + +export const VideoBlock: Block = { + slug: 'video-block', + labels: { + singular: 'Video', + plural: 'Videos', + }, + fields: [ + { + name: 'videoUrl', + type: 'text', + required: true, + label: 'Video-URL', + admin: { + description: 'YouTube oder Vimeo URL', + }, + }, + { + name: 'caption', + type: 'text', + label: 'Beschriftung', + localized: true, + }, + { + name: 'aspectRatio', + type: 'select', + defaultValue: '16:9', + label: 'Seitenverhältnis', + options: [ + { label: '16:9', value: '16:9' }, + { label: '4:3', value: '4:3' }, + { label: '1:1', value: '1:1' }, + ], + }, + ], +} diff --git a/src/blocks/index.ts b/src/blocks/index.ts new file mode 100644 index 0000000..54d72ab --- /dev/null +++ b/src/blocks/index.ts @@ -0,0 +1,16 @@ +export { HeroBlock } from './HeroBlock' +export { TextBlock } from './TextBlock' +export { ImageTextBlock } from './ImageTextBlock' +export { CardGridBlock } from './CardGridBlock' +export { QuoteBlock } from './QuoteBlock' +export { CTABlock } from './CTABlock' +export { ContactFormBlock } from './ContactFormBlock' +export { TimelineBlock } from './TimelineBlock' +export { DividerBlock } from './DividerBlock' +export { VideoBlock } from './VideoBlock' + +// Neue universelle Blocks +export { PostsListBlock } from './PostsListBlock' +export { TestimonialsBlock } from './TestimonialsBlock' +export { NewsletterBlock } from './NewsletterBlock' +export { ProcessStepsBlock } from './ProcessStepsBlock' diff --git a/src/globals/Navigation.ts b/src/globals/Navigation.ts new file mode 100644 index 0000000..76dd196 --- /dev/null +++ b/src/globals/Navigation.ts @@ -0,0 +1,131 @@ +import type { GlobalConfig } from 'payload' + +export const Navigation: GlobalConfig = { + slug: 'navigation', + label: 'Navigation', + access: { + read: () => true, + update: ({ req }) => !!req.user, + }, + fields: [ + { + name: 'mainMenu', + type: 'array', + label: 'Hauptmenü', + fields: [ + { + name: 'label', + type: 'text', + required: true, + localized: true, + }, + { + name: 'type', + type: 'select', + defaultValue: 'page', + options: [ + { label: 'Seite', value: 'page' }, + { label: 'Eigener Link', value: 'custom' }, + { label: 'Untermenü', value: 'submenu' }, + ], + }, + { + name: 'page', + type: 'relationship', + relationTo: 'pages', + admin: { + condition: (data, siblingData) => siblingData?.type === 'page', + }, + }, + { + name: 'url', + type: 'text', + admin: { + condition: (data, siblingData) => siblingData?.type === 'custom', + }, + }, + { + name: 'openInNewTab', + type: 'checkbox', + defaultValue: false, + }, + { + name: 'submenu', + type: 'array', + admin: { + condition: (data, siblingData) => siblingData?.type === 'submenu', + }, + fields: [ + { + name: 'label', + type: 'text', + required: true, + localized: true, + }, + { + name: 'linkType', + type: 'select', + defaultValue: 'page', + options: [ + { label: 'Seite', value: 'page' }, + { label: 'Eigener Link', value: 'custom' }, + ], + }, + { + name: 'page', + type: 'relationship', + relationTo: 'pages', + admin: { + condition: (data, siblingData) => siblingData?.linkType === 'page', + }, + }, + { + name: 'url', + type: 'text', + admin: { + condition: (data, siblingData) => siblingData?.linkType === 'custom', + }, + }, + ], + }, + ], + }, + { + name: 'footerMenu', + type: 'array', + label: 'Footer-Menü', + fields: [ + { + name: 'label', + type: 'text', + required: true, + localized: true, + }, + { + name: 'linkType', + type: 'select', + defaultValue: 'page', + options: [ + { label: 'Seite', value: 'page' }, + { label: 'Eigener Link', value: 'custom' }, + ], + }, + { + name: 'page', + type: 'relationship', + relationTo: 'pages', + admin: { + condition: (data, siblingData) => siblingData?.linkType === 'page', + }, + }, + { + name: 'url', + type: 'text', + admin: { + condition: (data, siblingData) => siblingData?.linkType === 'custom', + }, + }, + ], + }, + ], +} diff --git a/src/globals/SEOSettings.ts b/src/globals/SEOSettings.ts new file mode 100644 index 0000000..e0be527 --- /dev/null +++ b/src/globals/SEOSettings.ts @@ -0,0 +1,385 @@ +import type { GlobalConfig } from 'payload' + +/** + * SEO Settings Global + * + * Globale SEO-Einstellungen pro Tenant für: + * - Meta-Defaults + * - Social Media Profile + * - Schema.org Organisation + * - robots.txt Regeln + */ +export const SEOSettings: GlobalConfig = { + slug: 'seo-settings', + label: 'SEO Einstellungen', + admin: { + group: 'Einstellungen', + description: 'Globale SEO-Konfiguration und Schema.org Daten', + }, + fields: [ + // === META DEFAULTS === + { + name: 'metaDefaults', + type: 'group', + label: 'Standard Meta-Tags', + fields: [ + { + name: 'titleSuffix', + type: 'text', + label: 'Titel-Suffix', + defaultValue: '| Website', + localized: true, + admin: { + description: 'Wird an jeden Seitentitel angehängt (z.B. "Startseite | Firmenname")', + }, + }, + { + name: 'defaultDescription', + type: 'textarea', + label: 'Standard Meta-Beschreibung', + maxLength: 160, + localized: true, + admin: { + description: 'Wird verwendet, wenn keine spezifische Beschreibung gesetzt ist', + }, + }, + { + name: 'defaultOgImage', + type: 'upload', + relationTo: 'media', + label: 'Standard Social Media Bild', + admin: { + description: 'Fallback-Bild für Social Media Shares (empfohlen: 1200x630px)', + }, + }, + { + name: 'keywords', + type: 'text', + hasMany: true, + label: 'Standard Keywords', + admin: { + description: 'Globale Keywords (optional, geringe SEO-Relevanz)', + }, + }, + ], + }, + + // === ORGANIZATION SCHEMA === + { + name: 'organization', + type: 'group', + label: 'Organisation (Schema.org)', + admin: { + description: 'Daten für das Organization Schema', + }, + fields: [ + { + name: 'name', + type: 'text', + label: 'Firmenname', + required: true, + }, + { + name: 'legalName', + type: 'text', + label: 'Rechtlicher Name', + admin: { + description: 'Vollständiger rechtlicher Firmenname (falls abweichend)', + }, + }, + { + name: 'description', + type: 'textarea', + label: 'Unternehmensbeschreibung', + maxLength: 300, + localized: true, + }, + { + name: 'logo', + type: 'upload', + relationTo: 'media', + label: 'Logo', + admin: { + description: 'Firmenlogo für Schema.org (min. 112x112px, empfohlen: 512x512px)', + }, + }, + { + name: 'foundingDate', + type: 'date', + label: 'Gründungsdatum', + admin: { + date: { + pickerAppearance: 'dayOnly', + }, + }, + }, + ], + }, + + // === CONTACT INFO === + { + name: 'contact', + type: 'group', + label: 'Kontaktdaten', + fields: [ + { + name: 'email', + type: 'email', + label: 'E-Mail', + }, + { + name: 'phone', + type: 'text', + label: 'Telefon', + admin: { + description: 'Im Format +49 123 456789', + }, + }, + { + name: 'fax', + type: 'text', + label: 'Fax', + }, + ], + }, + + // === ADDRESS === + { + name: 'address', + type: 'group', + label: 'Adresse', + fields: [ + { + name: 'street', + type: 'text', + label: 'Straße & Hausnummer', + }, + { + name: 'postalCode', + type: 'text', + label: 'Postleitzahl', + }, + { + name: 'city', + type: 'text', + label: 'Stadt', + }, + { + name: 'region', + type: 'text', + label: 'Bundesland/Region', + }, + { + name: 'country', + type: 'text', + label: 'Land', + defaultValue: 'Deutschland', + }, + { + name: 'countryCode', + type: 'text', + label: 'Ländercode', + defaultValue: 'DE', + admin: { + description: 'ISO 3166-1 Alpha-2 Code', + }, + }, + ], + }, + + // === GEO LOCATION === + { + name: 'geo', + type: 'group', + label: 'Geo-Koordinaten', + admin: { + description: 'Für Local Business Schema', + }, + fields: [ + { + name: 'latitude', + type: 'number', + label: 'Breitengrad', + admin: { + step: 0.000001, + }, + }, + { + name: 'longitude', + type: 'number', + label: 'Längengrad', + admin: { + step: 0.000001, + }, + }, + ], + }, + + // === SOCIAL PROFILES === + { + name: 'socialProfiles', + type: 'array', + label: 'Social Media Profile', + admin: { + description: 'URLs zu Social Media Profilen (für sameAs Schema)', + }, + fields: [ + { + name: 'platform', + type: 'select', + label: 'Plattform', + options: [ + { label: 'Facebook', value: 'facebook' }, + { label: 'Instagram', value: 'instagram' }, + { label: 'Twitter/X', value: 'twitter' }, + { label: 'LinkedIn', value: 'linkedin' }, + { label: 'YouTube', value: 'youtube' }, + { label: 'TikTok', value: 'tiktok' }, + { label: 'Pinterest', value: 'pinterest' }, + { label: 'XING', value: 'xing' }, + { label: 'Andere', value: 'other' }, + ], + }, + { + name: 'url', + type: 'text', + label: 'Profil-URL', + required: true, + }, + ], + }, + + // === LOCAL BUSINESS === + { + name: 'localBusiness', + type: 'group', + label: 'Local Business', + admin: { + description: 'Zusätzliche Daten für lokale Unternehmen', + }, + fields: [ + { + name: 'enabled', + type: 'checkbox', + label: 'Local Business Schema aktivieren', + defaultValue: false, + }, + { + name: 'type', + type: 'select', + label: 'Geschäftstyp', + options: [ + { label: 'Lokales Unternehmen', value: 'LocalBusiness' }, + { label: 'Arztpraxis', value: 'Physician' }, + { label: 'Zahnarzt', value: 'Dentist' }, + { label: 'Anwaltskanzlei', value: 'Attorney' }, + { label: 'Restaurant', value: 'Restaurant' }, + { label: 'Hotel', value: 'Hotel' }, + { label: 'Einzelhandel', value: 'Store' }, + { label: 'Fitnessstudio', value: 'HealthClub' }, + { label: 'Friseursalon', value: 'HairSalon' }, + { label: 'Autowerkstatt', value: 'AutoRepair' }, + { label: 'Immobilienmakler', value: 'RealEstateAgent' }, + { label: 'Finanzdienstleister', value: 'FinancialService' }, + { label: 'IT-Dienstleister', value: 'ProfessionalService' }, + { label: 'Pflegedienst', value: 'MedicalBusiness' }, + ], + admin: { + condition: (data) => data?.localBusiness?.enabled, + }, + }, + { + name: 'priceRange', + type: 'select', + label: 'Preiskategorie', + options: [ + { label: '€ (Günstig)', value: '€' }, + { label: '€€ (Mittel)', value: '€€' }, + { label: '€€€ (Gehoben)', value: '€€€' }, + { label: '€€€€ (Premium)', value: '€€€€' }, + ], + admin: { + condition: (data) => data?.localBusiness?.enabled, + }, + }, + { + name: 'openingHours', + type: 'array', + label: 'Öffnungszeiten', + admin: { + condition: (data) => data?.localBusiness?.enabled, + description: 'Im Format "Mo-Fr 09:00-17:00"', + }, + fields: [ + { + name: 'specification', + type: 'text', + label: 'Öffnungszeit', + admin: { + placeholder: 'Mo-Fr 09:00-17:00', + }, + }, + ], + }, + ], + }, + + // === ROBOTS SETTINGS === + { + name: 'robots', + type: 'group', + label: 'Robots & Indexierung', + fields: [ + { + name: 'allowIndexing', + type: 'checkbox', + label: 'Indexierung erlauben', + defaultValue: true, + admin: { + description: 'Wenn deaktiviert, wird die gesamte Website von Suchmaschinen ausgeschlossen', + }, + }, + { + name: 'additionalDisallow', + type: 'text', + hasMany: true, + label: 'Zusätzliche Pfade ausschließen', + admin: { + description: 'Pfade die nicht gecrawlt werden sollen (z.B. "/intern", "/preview")', + condition: (data) => data?.robots?.allowIndexing, + }, + }, + ], + }, + + // === VERIFICATION CODES === + { + name: 'verification', + type: 'group', + label: 'Verifizierungscodes', + admin: { + description: 'Codes für Suchmaschinen-Verifizierung', + }, + fields: [ + { + name: 'google', + type: 'text', + label: 'Google Search Console', + admin: { + description: 'Meta-Tag Content (nur der Code, nicht das gesamte Tag)', + }, + }, + { + name: 'bing', + type: 'text', + label: 'Bing Webmaster Tools', + }, + { + name: 'yandex', + type: 'text', + label: 'Yandex Webmaster', + }, + ], + }, + ], +} diff --git a/src/globals/SiteSettings.ts b/src/globals/SiteSettings.ts new file mode 100644 index 0000000..7327c1e --- /dev/null +++ b/src/globals/SiteSettings.ts @@ -0,0 +1,92 @@ +import type { GlobalConfig } from 'payload' + +export const SiteSettings: GlobalConfig = { + slug: 'site-settings', + label: 'Site Settings', + access: { + read: () => true, + update: ({ req }) => !!req.user, + }, + fields: [ + { + name: 'siteName', + type: 'text', + defaultValue: 'porwoll.de', + localized: true, + }, + { + name: 'siteTagline', + type: 'text', + localized: true, + }, + { + name: 'logo', + type: 'upload', + relationTo: 'media', + }, + { + name: 'favicon', + type: 'upload', + relationTo: 'media', + }, + { + name: 'contact', + type: 'group', + fields: [ + { + name: 'email', + type: 'email', + }, + { + name: 'phone', + type: 'text', + }, + { + name: 'address', + type: 'textarea', + }, + ], + }, + { + name: 'footer', + type: 'group', + fields: [ + { + name: 'copyrightText', + type: 'text', + localized: true, + }, + { + name: 'showSocialLinks', + type: 'checkbox', + defaultValue: true, + }, + ], + }, + { + name: 'seo', + type: 'group', + label: 'SEO', + fields: [ + { + name: 'defaultMetaTitle', + type: 'text', + label: 'Standard Meta-Titel', + localized: true, + }, + { + name: 'defaultMetaDescription', + type: 'textarea', + label: 'Standard Meta-Beschreibung', + localized: true, + }, + { + name: 'defaultOgImage', + type: 'upload', + relationTo: 'media', + label: 'Standard Social Media Bild', + }, + ], + }, + ], +}