feat: add comprehensive blogging and team features

Blogging Collections:
- Tags Collection with name, slug, description, color
- Authors Collection with avatar, bio, social media links

Posts Collection extended:
- Tags and Author relationships
- Co-Authors support
- Automatic reading time calculation
- Legacy author text field fallback

New Blogging Blocks:
- AuthorBioBlock: Display author info with various layouts
- RelatedPostsBlock: Show related articles (auto/manual/category/tag)
- ShareButtonsBlock: Social sharing (Facebook, Twitter, LinkedIn, etc.)
- TableOfContentsBlock: Auto-generated TOC from headings

Team Collection extended:
- Slug field for profile pages (auto-generated)
- Hierarchy fields (reportsTo, hierarchyLevel) for org charts
- vCard export flag

New Team API Endpoints:
- GET /api/team - List with search and filters
- GET /api/team/[slug]/vcard - vCard download (VCF)

New Team Blocks:
- TeamFilterBlock: Interactive team display with search/filter
- OrgChartBlock: Hierarchical organization chart visualization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-13 21:49:13 +00:00
parent f0424a4abf
commit 2b097eefb3
18 changed files with 4858 additions and 2 deletions

View file

@ -0,0 +1,187 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
/**
* vCard Export API
*
* GET /api/team/[slug]/vcard - Generiert vCard (VCF) für ein Team-Mitglied
*
* Query-Parameter:
* - tenant (required): Tenant ID
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await params
const { searchParams } = new URL(request.url)
// Required: Tenant
const tenantId = searchParams.get('tenant')
if (!tenantId) {
return NextResponse.json({ error: 'tenant parameter is required' }, { status: 400 })
}
const payload = await getPayload({ config })
// Find team member
const result = await payload.find({
collection: 'team',
where: {
tenant: { equals: parseInt(tenantId) },
slug: { equals: slug },
isActive: { equals: true },
},
depth: 1,
limit: 1,
})
if (result.docs.length === 0) {
return NextResponse.json({ error: 'Team member not found' }, { status: 404 })
}
const member = result.docs[0]
// Check if vCard export is allowed
if (member.allowVCard === false) {
return NextResponse.json({ error: 'vCard export not allowed for this member' }, { status: 403 })
}
// Generate vCard 3.0
const vcard = generateVCard(member)
// Return as downloadable file
const filename = `${member.slug || member.name?.toLowerCase().replace(/\s+/g, '-')}.vcf`
return new NextResponse(vcard, {
status: 200,
headers: {
'Content-Type': 'text/vcard; charset=utf-8',
'Content-Disposition': `attachment; filename="${filename}"`,
},
})
} catch (error) {
console.error('vCard API error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
interface TeamMember {
name?: string
role?: string
department?: string
email?: string
phone?: string
showContactInfo?: boolean
image?: { url?: string } | number
socialLinks?: Array<{ platform?: string; url?: string }>
qualifications?: Array<{ title?: string }>
specializations?: Array<{ title?: string }>
languages?: Array<{ language?: string; level?: string }>
bioShort?: string
}
function generateVCard(member: TeamMember): string {
const lines: string[] = []
// vCard header
lines.push('BEGIN:VCARD')
lines.push('VERSION:3.0')
// Name
if (member.name) {
const nameParts = member.name.split(' ')
const lastName = nameParts.pop() || ''
const firstName = nameParts.join(' ')
lines.push(`N:${escapeVCard(lastName)};${escapeVCard(firstName)};;;`)
lines.push(`FN:${escapeVCard(member.name)}`)
}
// Organization & Title
if (member.role) {
lines.push(`TITLE:${escapeVCard(member.role)}`)
}
if (member.department) {
lines.push(`ORG:;${escapeVCard(member.department)}`)
}
// Contact info (only if allowed)
if (member.showContactInfo) {
if (member.email) {
lines.push(`EMAIL;TYPE=WORK:${member.email}`)
}
if (member.phone) {
lines.push(`TEL;TYPE=WORK:${member.phone}`)
}
}
// Photo URL (if available)
if (member.image && typeof member.image === 'object' && member.image.url) {
lines.push(`PHOTO;VALUE=URI:${member.image.url}`)
}
// Social Links as URLs
if (Array.isArray(member.socialLinks)) {
member.socialLinks.forEach((link, index) => {
if (link.url) {
const label = link.platform?.toUpperCase() || `SOCIAL${index + 1}`
lines.push(`URL;TYPE=${label}:${link.url}`)
}
})
}
// Note with bio and qualifications
const notes: string[] = []
if (member.bioShort) {
notes.push(member.bioShort)
}
if (Array.isArray(member.qualifications) && member.qualifications.length > 0) {
const quals = member.qualifications.map((q) => q.title).filter(Boolean).join(', ')
if (quals) {
notes.push(`Qualifikationen: ${quals}`)
}
}
if (Array.isArray(member.specializations) && member.specializations.length > 0) {
const specs = member.specializations.map((s) => s.title).filter(Boolean).join(', ')
if (specs) {
notes.push(`Fachgebiete: ${specs}`)
}
}
if (Array.isArray(member.languages) && member.languages.length > 0) {
const langs = member.languages.map((l) => l.language).filter(Boolean).join(', ')
if (langs) {
notes.push(`Sprachen: ${langs}`)
}
}
if (notes.length > 0) {
lines.push(`NOTE:${escapeVCard(notes.join('\\n'))}`)
}
// Categories (specializations as tags)
if (Array.isArray(member.specializations) && member.specializations.length > 0) {
const categories = member.specializations.map((s) => s.title).filter(Boolean).join(',')
if (categories) {
lines.push(`CATEGORIES:${escapeVCard(categories)}`)
}
}
// Production ID and revision
lines.push(`PRODID:-//Payload CMS//Team vCard//DE`)
lines.push(`REV:${new Date().toISOString().replace(/[-:]/g, '').split('.')[0]}Z`)
// vCard footer
lines.push('END:VCARD')
return lines.join('\r\n')
}
function escapeVCard(text: string): string {
if (!text) return ''
return text
.replace(/\\/g, '\\\\')
.replace(/;/g, '\\;')
.replace(/,/g, '\\,')
.replace(/\n/g, '\\n')
}

View file

@ -0,0 +1,214 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
/**
* Team API
*
* GET /api/team - Liste aller Team-Mitglieder mit Filter und Suche
*
* Query-Parameter:
* - tenant (required): Tenant ID
* - slug: Einzelnes Mitglied nach Slug
* - search: Volltextsuche in Name, Rolle, Abteilung, Bio
* - department: Nach Abteilung filtern
* - level: Nach Hierarchie-Ebene filtern
* - specialization: Nach Fachgebiet filtern
* - language: Nach Sprache filtern
* - featured: Nur hervorgehobene (true/false)
* - limit: Maximale Anzahl (default: 50)
* - page: Seite für Pagination
* - sort: Sortierung (order, name, department, startDate)
* - locale: Sprache (de/en)
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
// Required: Tenant
const tenantId = searchParams.get('tenant')
if (!tenantId) {
return NextResponse.json({ error: 'tenant parameter is required' }, { status: 400 })
}
const payload = await getPayload({ config })
// Optional parameters
const slug = searchParams.get('slug')
const search = searchParams.get('search')
const department = searchParams.get('department')
const level = searchParams.get('level')
const specialization = searchParams.get('specialization')
const language = searchParams.get('language')
const featured = searchParams.get('featured')
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 100)
const page = parseInt(searchParams.get('page') || '1')
const sort = searchParams.get('sort') || 'order'
const locale = (searchParams.get('locale') as 'de' | 'en') || 'de'
// Build where clause
const where: Record<string, unknown> = {
tenant: { equals: parseInt(tenantId) },
isActive: { equals: true },
}
// Single member by slug
if (slug) {
where.slug = { equals: slug }
}
// Department filter
if (department) {
where.department = { contains: department }
}
// Hierarchy level filter
if (level) {
where.hierarchyLevel = { equals: level }
}
// Featured filter
if (featured === 'true') {
where.isFeatured = { equals: true }
}
// Build sort string
let sortField = 'order'
switch (sort) {
case 'name':
sortField = 'name'
break
case 'department':
sortField = 'department'
break
case 'startDate':
sortField = '-startDate'
break
case '-order':
sortField = '-order'
break
default:
sortField = 'order'
}
// Query team members
const result = await payload.find({
collection: 'team',
where,
sort: sortField,
limit,
page,
locale,
depth: 2, // Include image and reportsTo
})
let members = result.docs
// Post-query filters (for array fields)
// Search filter (case-insensitive)
if (search) {
const searchLower = search.toLowerCase()
members = members.filter((member) => {
const nameMatch = member.name?.toLowerCase().includes(searchLower)
const roleMatch =
typeof member.role === 'string' && member.role.toLowerCase().includes(searchLower)
const deptMatch =
typeof member.department === 'string' &&
member.department.toLowerCase().includes(searchLower)
const bioMatch =
typeof member.bioShort === 'string' &&
member.bioShort.toLowerCase().includes(searchLower)
// Search in specializations
const specMatch =
Array.isArray(member.specializations) &&
member.specializations.some(
(s) => typeof s.title === 'string' && s.title.toLowerCase().includes(searchLower)
)
return nameMatch || roleMatch || deptMatch || bioMatch || specMatch
})
}
// Specialization filter
if (specialization) {
const specLower = specialization.toLowerCase()
members = members.filter(
(member) =>
Array.isArray(member.specializations) &&
member.specializations.some(
(s) => typeof s.title === 'string' && s.title.toLowerCase().includes(specLower)
)
)
}
// Language filter
if (language) {
const langLower = language.toLowerCase()
members = members.filter(
(member) =>
Array.isArray(member.languages) &&
member.languages.some(
(l) => typeof l.language === 'string' && l.language.toLowerCase().includes(langLower)
)
)
}
// Get unique departments for filter dropdown
const allMembers = await payload.find({
collection: 'team',
where: {
tenant: { equals: parseInt(tenantId) },
isActive: { equals: true },
},
limit: 1000,
locale,
})
const departments = [
...new Set(allMembers.docs.map((m) => m.department).filter(Boolean)),
].sort() as string[]
const specializations = [
...new Set(
allMembers.docs.flatMap((m) =>
Array.isArray(m.specializations) ? m.specializations.map((s) => s.title) : []
)
),
]
.filter(Boolean)
.sort() as string[]
const languages = [
...new Set(
allMembers.docs.flatMap((m) =>
Array.isArray(m.languages) ? m.languages.map((l) => l.language) : []
)
),
]
.filter(Boolean)
.sort() as string[]
// Single member response
if (slug && members.length === 1) {
return NextResponse.json({
member: members[0],
filters: { departments, specializations, languages },
})
}
return NextResponse.json({
members,
totalDocs: result.totalDocs,
totalPages: result.totalPages,
page: result.page,
hasNextPage: result.hasNextPage,
hasPrevPage: result.hasPrevPage,
filters: { departments, specializations, languages },
})
} catch (error) {
console.error('Team API error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View file

@ -0,0 +1,206 @@
import type { Block } from 'payload'
/**
* AuthorBioBlock
*
* Zeigt Autoren-Informationen am Ende eines Blog-Posts an.
* Kann automatisch den Artikel-Autor oder manuell ausgewählte Autoren anzeigen.
*/
export const AuthorBioBlock: Block = {
slug: 'author-bio-block',
labels: {
singular: 'Autoren-Bio',
plural: 'Autoren-Bios',
},
imageURL: '/assets/blocks/author-bio.png',
fields: [
{
name: 'source',
type: 'select',
defaultValue: 'post',
label: 'Autoren-Quelle',
options: [
{ label: 'Autor des Artikels', value: 'post' },
{ label: 'Manuell auswählen', value: 'manual' },
],
admin: {
description: 'Woher sollen die Autoren-Daten kommen?',
},
},
{
name: 'authors',
type: 'relationship',
relationTo: 'authors',
hasMany: true,
label: 'Autoren',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
description: 'Wählen Sie die anzuzeigenden Autoren',
},
},
{
name: 'showCoAuthors',
type: 'checkbox',
defaultValue: true,
label: 'Co-Autoren anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'post',
description: 'Auch Co-Autoren des Artikels anzeigen',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'card',
label: 'Layout',
options: [
{ label: 'Karte', value: 'card' },
{ label: 'Inline', value: 'inline' },
{ label: 'Kompakt', value: 'compact' },
{ label: 'Feature (Groß)', value: 'feature' },
],
},
// Anzuzeigende Elemente
{
name: 'show',
type: 'group',
label: 'Anzeigen',
fields: [
{
name: 'avatar',
type: 'checkbox',
defaultValue: true,
label: 'Profilbild',
},
{
name: 'name',
type: 'checkbox',
defaultValue: true,
label: 'Name',
},
{
name: 'title',
type: 'checkbox',
defaultValue: true,
label: 'Titel/Position',
},
{
name: 'bio',
type: 'select',
defaultValue: 'short',
label: 'Biografie',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Kurz', value: 'short' },
{ label: 'Vollständig', value: 'full' },
],
},
{
name: 'social',
type: 'checkbox',
defaultValue: true,
label: 'Social Media Links',
},
{
name: 'email',
type: 'checkbox',
defaultValue: false,
label: 'E-Mail',
},
{
name: 'website',
type: 'checkbox',
defaultValue: false,
label: 'Website',
},
{
name: 'postCount',
type: 'checkbox',
defaultValue: false,
label: 'Anzahl Artikel',
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'avatarSize',
type: 'select',
defaultValue: 'md',
label: 'Avatar-Größe',
options: [
{ label: 'Klein (48px)', value: 'sm' },
{ label: 'Mittel (80px)', value: 'md' },
{ label: 'Groß (120px)', value: 'lg' },
],
},
{
name: 'avatarShape',
type: 'select',
defaultValue: 'circle',
label: 'Avatar-Form',
options: [
{ label: 'Rund', value: 'circle' },
{ label: 'Quadratisch', value: 'square' },
{ label: 'Abgerundet', value: 'rounded' },
],
},
{
name: 'bg',
type: 'select',
defaultValue: 'light',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Akzent', value: 'accent' },
],
},
{
name: 'border',
type: 'checkbox',
defaultValue: false,
label: 'Rahmen',
},
{
name: 'shadow',
type: 'checkbox',
defaultValue: false,
label: 'Schatten',
},
{
name: 'divider',
type: 'checkbox',
defaultValue: true,
label: 'Trennlinie oben',
admin: {
description: 'Linie zur Trennung vom Artikel-Inhalt',
},
},
],
},
// Label
{
name: 'label',
type: 'text',
label: 'Überschrift',
localized: true,
admin: {
description: 'z.B. "Über den Autor", "Geschrieben von" (optional)',
},
},
// Link zur Autorenseite
{
name: 'linkToProfile',
type: 'checkbox',
defaultValue: true,
label: 'Zur Autorenseite verlinken',
},
],
}

455
src/blocks/OrgChartBlock.ts Normal file
View file

@ -0,0 +1,455 @@
import type { Block } from 'payload'
/**
* OrgChartBlock (Organigramm)
*
* Hierarchische Darstellung der Team-Struktur.
* Nutzt reportsTo und hierarchyLevel aus der Team Collection.
*/
export const OrgChartBlock: Block = {
slug: 'org-chart-block',
labels: {
singular: 'Organigramm',
plural: 'Organigramme',
},
imageURL: '/assets/blocks/org-chart.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Unsere Struktur',
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
// Datenquelle
{
name: 'source',
type: 'select',
defaultValue: 'auto',
label: 'Datenquelle',
options: [
{ label: 'Automatisch (aus Hierarchie)', value: 'auto' },
{ label: 'Nach Abteilung', value: 'department' },
{ label: 'Manuell auswählen', value: 'manual' },
],
},
{
name: 'rootMember',
type: 'relationship',
relationTo: 'team',
label: 'Wurzel-Person',
admin: {
condition: (_, siblingData) => siblingData?.source === 'auto',
description: 'Oberstes Element (z.B. Geschäftsführer). Leer = automatisch ermitteln.',
},
},
{
name: 'department',
type: 'text',
label: 'Abteilung',
admin: {
condition: (_, siblingData) => siblingData?.source === 'department',
description: 'Zeigt nur diese Abteilung',
},
},
{
name: 'selectedMembers',
type: 'relationship',
relationTo: 'team',
hasMany: true,
label: 'Mitglieder auswählen',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
},
},
{
name: 'maxDepth',
type: 'number',
defaultValue: 5,
min: 1,
max: 10,
label: 'Maximale Tiefe',
admin: {
description: 'Wie viele Hierarchie-Ebenen anzeigen',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'tree',
label: 'Layout',
options: [
{ label: 'Baum (vertikal)', value: 'tree' },
{ label: 'Baum (horizontal)', value: 'tree-horizontal' },
{ label: 'Organigramm (klassisch)', value: 'org' },
{ label: 'Radial/Kreis', value: 'radial' },
{ label: 'Ebenen (gestapelt)', value: 'layers' },
],
},
{
name: 'direction',
type: 'select',
defaultValue: 'top',
label: 'Richtung',
options: [
{ label: 'Von oben nach unten', value: 'top' },
{ label: 'Von unten nach oben', value: 'bottom' },
{ label: 'Von links nach rechts', value: 'left' },
{ label: 'Von rechts nach links', value: 'right' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'tree' || siblingData?.layout === 'org',
},
},
// Node-Darstellung
{
name: 'node',
type: 'group',
label: 'Knoten-Darstellung',
fields: [
{
name: 'style',
type: 'select',
defaultValue: 'card',
label: 'Stil',
options: [
{ label: 'Karte', value: 'card' },
{ label: 'Kompakt', value: 'compact' },
{ label: 'Nur Foto', value: 'avatar' },
{ label: 'Nur Text', value: 'text' },
],
},
{
name: 'showImage',
type: 'checkbox',
defaultValue: true,
label: 'Foto anzeigen',
},
{
name: 'imageSize',
type: 'select',
defaultValue: 'md',
label: 'Foto-Größe',
options: [
{ label: 'Klein (40px)', value: 'sm' },
{ label: 'Mittel (60px)', value: 'md' },
{ label: 'Groß (80px)', value: 'lg' },
],
admin: {
condition: (_, siblingData) => siblingData?.showImage,
},
},
{
name: 'imageShape',
type: 'select',
defaultValue: 'circle',
label: 'Foto-Form',
options: [
{ label: 'Rund', value: 'circle' },
{ label: 'Abgerundet', value: 'rounded' },
{ label: 'Quadratisch', value: 'square' },
],
admin: {
condition: (_, siblingData) => siblingData?.showImage,
},
},
{
name: 'showName',
type: 'checkbox',
defaultValue: true,
label: 'Name anzeigen',
},
{
name: 'showRole',
type: 'checkbox',
defaultValue: true,
label: 'Position anzeigen',
},
{
name: 'showDepartment',
type: 'checkbox',
defaultValue: false,
label: 'Abteilung anzeigen',
},
{
name: 'showContact',
type: 'checkbox',
defaultValue: false,
label: 'Kontakt anzeigen',
},
{
name: 'clickAction',
type: 'select',
defaultValue: 'modal',
label: 'Klick-Aktion',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Detail-Modal', value: 'modal' },
{ label: 'Zur Profilseite', value: 'link' },
{ label: 'Untergeordnete ein-/ausklappen', value: 'expand' },
],
},
{
name: 'profileBasePath',
type: 'text',
label: 'Profil-Basis-Pfad',
defaultValue: '/team',
admin: {
condition: (_, siblingData) => siblingData?.clickAction === 'link',
},
},
],
},
// Verbindungslinien
{
name: 'connectors',
type: 'group',
label: 'Verbindungslinien',
fields: [
{
name: 'style',
type: 'select',
defaultValue: 'straight',
label: 'Linien-Stil',
options: [
{ label: 'Gerade', value: 'straight' },
{ label: 'Abgewinkelt', value: 'angular' },
{ label: 'Gebogen', value: 'curved' },
{ label: 'Gestrichelt', value: 'dashed' },
],
},
{
name: 'color',
type: 'select',
defaultValue: 'gray',
label: 'Farbe',
options: [
{ label: 'Grau', value: 'gray' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Akzent', value: 'accent' },
{ label: 'Gradient', value: 'gradient' },
],
},
{
name: 'thickness',
type: 'select',
defaultValue: '2',
label: 'Dicke',
options: [
{ label: 'Dünn (1px)', value: '1' },
{ label: 'Normal (2px)', value: '2' },
{ label: 'Dick (3px)', value: '3' },
],
},
{
name: 'animated',
type: 'checkbox',
defaultValue: false,
label: 'Animation',
admin: {
description: 'Animierte Linien beim Laden',
},
},
],
},
// Hierarchie-Ebenen-Styling
{
name: 'levels',
type: 'group',
label: 'Ebenen-Styling',
fields: [
{
name: 'colorByLevel',
type: 'checkbox',
defaultValue: true,
label: 'Farblich unterscheiden',
admin: {
description: 'Verschiedene Farben pro Hierarchie-Ebene',
},
},
{
name: 'sizeByLevel',
type: 'checkbox',
defaultValue: true,
label: 'Größe variieren',
admin: {
description: 'Höhere Ebenen größer darstellen',
},
},
{
name: 'collapsible',
type: 'checkbox',
defaultValue: true,
label: 'Ebenen einklappbar',
},
{
name: 'initiallyExpanded',
type: 'number',
defaultValue: 2,
min: 1,
max: 5,
label: 'Initial ausgeklappt',
admin: {
condition: (_, siblingData) => siblingData?.collapsible,
description: 'Wie viele Ebenen anfangs sichtbar',
},
},
],
},
// Interaktion
{
name: 'interaction',
type: 'group',
label: 'Interaktion',
fields: [
{
name: 'zoomable',
type: 'checkbox',
defaultValue: true,
label: 'Zoom erlauben',
},
{
name: 'pannable',
type: 'checkbox',
defaultValue: true,
label: 'Verschieben erlauben',
},
{
name: 'minimap',
type: 'checkbox',
defaultValue: false,
label: 'Minimap anzeigen',
admin: {
description: 'Kleine Übersichtskarte bei großen Organigrammen',
},
},
{
name: 'search',
type: 'checkbox',
defaultValue: false,
label: 'Suche aktivieren',
},
{
name: 'highlight',
type: 'checkbox',
defaultValue: true,
label: 'Hover-Hervorhebung',
admin: {
description: 'Bei Hover Pfad zur Wurzel hervorheben',
},
},
{
name: 'fullscreen',
type: 'checkbox',
defaultValue: true,
label: 'Vollbild-Modus',
},
{
name: 'export',
type: 'checkbox',
defaultValue: false,
label: 'Export erlauben',
admin: {
description: 'Als PNG oder PDF exportieren',
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'light',
label: 'Hintergrund',
options: [
{ label: 'Transparent', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Weiß', value: 'white' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Gradient', value: 'gradient' },
],
},
{
name: 'nodeBg',
type: 'select',
defaultValue: 'white',
label: 'Knoten-Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Hell', value: 'light' },
{ label: 'Transparent', value: 'transparent' },
{ label: 'Akzent', value: 'accent' },
],
},
{
name: 'nodeShadow',
type: 'checkbox',
defaultValue: true,
label: 'Knoten-Schatten',
},
{
name: 'nodeBorder',
type: 'checkbox',
defaultValue: false,
label: 'Knoten-Rahmen',
},
{
name: 'spacing',
type: 'select',
defaultValue: 'md',
label: 'Abstände',
options: [
{ label: 'Kompakt', value: 'sm' },
{ label: 'Normal', value: 'md' },
{ label: 'Großzügig', value: 'lg' },
],
},
{
name: 'minHeight',
type: 'select',
defaultValue: '400',
label: 'Mindesthöhe',
options: [
{ label: 'Auto', value: 'auto' },
{ label: '300px', value: '300' },
{ label: '400px', value: '400' },
{ label: '500px', value: '500' },
{ label: '600px', value: '600' },
{ label: 'Vollbild', value: 'full' },
],
},
],
},
// Legend
{
name: 'showLegend',
type: 'checkbox',
defaultValue: false,
label: 'Legende anzeigen',
admin: {
description: 'Erklärt Farben und Symbole',
},
},
// Accessibility
{
name: 'a11yLabel',
type: 'text',
label: 'ARIA Label',
localized: true,
defaultValue: 'Organigramm der Unternehmensstruktur',
},
],
}

View file

@ -0,0 +1,263 @@
import type { Block } from 'payload'
/**
* RelatedPostsBlock
*
* Zeigt verwandte Artikel an, basierend auf Kategorien, Tags
* oder manueller Auswahl.
*/
export const RelatedPostsBlock: Block = {
slug: 'related-posts-block',
labels: {
singular: 'Ähnliche Artikel',
plural: 'Ähnliche Artikel',
},
imageURL: '/assets/blocks/related-posts.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Das könnte Sie auch interessieren',
},
{
name: 'source',
type: 'select',
defaultValue: 'auto',
label: 'Artikel-Quelle',
options: [
{ label: 'Automatisch (Kategorien/Tags)', value: 'auto' },
{ label: 'Manuell auswählen', value: 'manual' },
{ label: 'Nach Kategorie', value: 'category' },
{ label: 'Nach Tag', value: 'tag' },
{ label: 'Neueste Artikel', value: 'latest' },
{ label: 'Beliebteste Artikel', value: 'popular' },
{ label: 'Vom gleichen Autor', value: 'author' },
],
},
{
name: 'posts',
type: 'relationship',
relationTo: 'posts',
hasMany: true,
label: 'Artikel',
admin: {
condition: (_, siblingData) => siblingData?.source === 'manual',
description: 'Wählen Sie die anzuzeigenden Artikel',
},
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
label: 'Kategorie',
admin: {
condition: (_, siblingData) => siblingData?.source === 'category',
},
},
{
name: 'tag',
type: 'relationship',
relationTo: 'tags',
label: 'Tag',
admin: {
condition: (_, siblingData) => siblingData?.source === 'tag',
},
},
{
name: 'limit',
type: 'number',
defaultValue: 3,
min: 1,
max: 12,
label: 'Anzahl Artikel',
},
{
name: 'excludeCurrent',
type: 'checkbox',
defaultValue: true,
label: 'Aktuellen Artikel ausschließen',
admin: {
description: 'Den Artikel, auf dem dieser Block ist, nicht anzeigen',
},
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid', value: 'grid' },
{ label: 'Liste', value: 'list' },
{ label: 'Slider', value: 'slider' },
{ label: 'Kompakt', value: 'compact' },
{ label: 'Cards', value: 'cards' },
],
},
{
name: 'cols',
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 === 'cards',
},
},
// Anzuzeigende Elemente
{
name: 'show',
type: 'group',
label: 'Anzeigen',
fields: [
{
name: 'image',
type: 'checkbox',
defaultValue: true,
label: 'Beitragsbild',
},
{
name: 'date',
type: 'checkbox',
defaultValue: true,
label: 'Datum',
},
{
name: 'author',
type: 'checkbox',
defaultValue: false,
label: 'Autor',
},
{
name: 'category',
type: 'checkbox',
defaultValue: true,
label: 'Kategorie',
},
{
name: 'excerpt',
type: 'checkbox',
defaultValue: true,
label: 'Kurzfassung',
},
{
name: 'readingTime',
type: 'checkbox',
defaultValue: false,
label: 'Lesezeit',
},
{
name: 'tags',
type: 'checkbox',
defaultValue: false,
label: 'Tags',
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'imgRatio',
type: 'select',
defaultValue: '16-9',
label: 'Bild-Verhältnis',
options: [
{ label: 'Quadratisch (1:1)', value: 'square' },
{ label: 'Querformat (4:3)', value: '4-3' },
{ label: 'Breit (16:9)', value: '16-9' },
{ label: 'Hochformat (3:4)', value: '3-4' },
],
},
{
name: 'rounded',
type: 'select',
defaultValue: 'md',
label: 'Ecken',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Klein', value: 'sm' },
{ label: 'Mittel', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'shadow',
type: 'checkbox',
defaultValue: false,
label: 'Schatten',
},
{
name: 'hover',
type: 'select',
defaultValue: 'lift',
label: 'Hover-Effekt',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Anheben', value: 'lift' },
{ label: 'Zoom', value: 'zoom' },
{ label: 'Overlay', value: 'overlay' },
],
},
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'gap',
type: 'select',
defaultValue: '24',
label: 'Abstand',
options: [
{ label: 'Klein (16px)', value: '16' },
{ label: 'Normal (24px)', value: '24' },
{ label: 'Groß (32px)', value: '32' },
],
},
],
},
// CTA
{
name: 'showAllLink',
type: 'checkbox',
defaultValue: false,
label: '"Alle anzeigen" Link',
},
{
name: 'allLinkText',
type: 'text',
label: 'Link-Text',
localized: true,
defaultValue: 'Alle Artikel anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.showAllLink,
},
},
{
name: 'allLinkUrl',
type: 'text',
label: 'Link-URL',
admin: {
condition: (_, siblingData) => siblingData?.showAllLink,
description: 'z.B. /blog oder /news',
},
},
],
}

View file

@ -0,0 +1,306 @@
import type { Block } from 'payload'
/**
* ShareButtonsBlock
*
* Social Sharing Buttons für Blog-Posts und andere Inhalte.
* Unterstützt verschiedene Plattformen und Layouts.
*/
export const ShareButtonsBlock: Block = {
slug: 'share-buttons-block',
labels: {
singular: 'Teilen-Buttons',
plural: 'Teilen-Buttons',
},
imageURL: '/assets/blocks/share-buttons.png',
fields: [
{
name: 'label',
type: 'text',
label: 'Label',
localized: true,
admin: {
description: 'z.B. "Teilen:", "Artikel teilen" (optional)',
},
},
// Plattformen
{
name: 'platforms',
type: 'group',
label: 'Plattformen',
fields: [
{
name: 'facebook',
type: 'checkbox',
defaultValue: true,
label: 'Facebook',
},
{
name: 'twitter',
type: 'checkbox',
defaultValue: true,
label: 'Twitter/X',
},
{
name: 'linkedin',
type: 'checkbox',
defaultValue: true,
label: 'LinkedIn',
},
{
name: 'xing',
type: 'checkbox',
defaultValue: false,
label: 'Xing',
},
{
name: 'whatsapp',
type: 'checkbox',
defaultValue: true,
label: 'WhatsApp',
},
{
name: 'telegram',
type: 'checkbox',
defaultValue: false,
label: 'Telegram',
},
{
name: 'email',
type: 'checkbox',
defaultValue: true,
label: 'E-Mail',
},
{
name: 'copy',
type: 'checkbox',
defaultValue: true,
label: 'Link kopieren',
},
{
name: 'print',
type: 'checkbox',
defaultValue: false,
label: 'Drucken',
},
{
name: 'pinterest',
type: 'checkbox',
defaultValue: false,
label: 'Pinterest',
},
{
name: 'reddit',
type: 'checkbox',
defaultValue: false,
label: 'Reddit',
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'horizontal',
label: 'Layout',
options: [
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Vertikal', value: 'vertical' },
{ label: 'Floating (Seitenrand)', value: 'floating' },
{ label: 'Sticky (unten)', value: 'sticky' },
],
},
{
name: 'align',
type: 'select',
defaultValue: 'left',
label: 'Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Mitte', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.layout === 'horizontal' || siblingData?.layout === 'vertical',
},
},
{
name: 'floatSide',
type: 'select',
defaultValue: 'left',
label: 'Seite',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
],
admin: {
condition: (_, siblingData) => siblingData?.layout === 'floating',
},
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'variant',
type: 'select',
defaultValue: 'filled',
label: 'Button-Stil',
options: [
{ label: 'Gefüllt', value: 'filled' },
{ label: 'Outline', value: 'outline' },
{ label: 'Nur Icon', value: 'icon' },
{ label: 'Minimal', value: 'minimal' },
],
},
{
name: 'size',
type: 'select',
defaultValue: 'md',
label: 'Größe',
options: [
{ label: 'Klein', value: 'sm' },
{ label: 'Mittel', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'shape',
type: 'select',
defaultValue: 'rounded',
label: 'Form',
options: [
{ label: 'Eckig', value: 'square' },
{ label: 'Abgerundet', value: 'rounded' },
{ label: 'Rund', value: 'circle' },
{ label: 'Pill', value: 'pill' },
],
},
{
name: 'colorScheme',
type: 'select',
defaultValue: 'brand',
label: 'Farbschema',
options: [
{ label: 'Plattform-Farben', value: 'brand' },
{ label: 'Einheitlich (Grau)', value: 'gray' },
{ label: 'Einheitlich (Dunkel)', value: 'dark' },
{ label: 'Einheitlich (Hell)', value: 'light' },
{ label: 'Theme-Akzent', value: 'accent' },
],
},
{
name: 'gap',
type: 'select',
defaultValue: '8',
label: 'Abstand',
options: [
{ label: 'Kein Abstand', value: '0' },
{ label: 'Klein (4px)', value: '4' },
{ label: 'Normal (8px)', value: '8' },
{ label: 'Groß (12px)', value: '12' },
],
},
{
name: 'showLabel',
type: 'checkbox',
defaultValue: false,
label: 'Plattform-Namen anzeigen',
},
{
name: 'showCount',
type: 'checkbox',
defaultValue: false,
label: 'Share-Zähler anzeigen',
admin: {
description: 'Zeigt Anzahl der Shares (sofern verfügbar)',
},
},
],
},
// Verhalten
{
name: 'behavior',
type: 'group',
label: 'Verhalten',
fields: [
{
name: 'openInPopup',
type: 'checkbox',
defaultValue: true,
label: 'In Popup öffnen',
admin: {
description: 'Share-Dialoge in kleinem Popup statt neuem Tab',
},
},
{
name: 'useNativeShare',
type: 'checkbox',
defaultValue: true,
label: 'Native Share API nutzen',
admin: {
description: 'Auf mobilen Geräten den System-Share-Dialog verwenden',
},
},
{
name: 'copyFeedback',
type: 'text',
label: 'Kopiert-Feedback',
localized: true,
defaultValue: 'Link kopiert!',
admin: {
description: 'Text der angezeigt wird nach dem Kopieren',
},
},
],
},
// Share-Inhalt
{
name: 'content',
type: 'group',
label: 'Share-Inhalt',
admin: {
description: 'Überschreibt automatische Werte (optional)',
},
fields: [
{
name: 'customTitle',
type: 'text',
label: 'Titel',
localized: true,
admin: {
description: 'Leer lassen für automatischen Seitentitel',
},
},
{
name: 'customDescription',
type: 'textarea',
label: 'Beschreibung',
localized: true,
admin: {
description: 'Leer lassen für automatische Meta-Description',
},
},
{
name: 'hashtags',
type: 'text',
label: 'Hashtags',
admin: {
description: 'Komma-getrennt, ohne # (z.B. "blog,news,tech")',
},
},
{
name: 'via',
type: 'text',
label: 'Twitter Via',
admin: {
description: 'Twitter-Handle ohne @ (z.B. "c2sgmbh")',
},
},
],
},
],
}

View file

@ -0,0 +1,287 @@
import type { Block } from 'payload'
/**
* TableOfContentsBlock
*
* Automatisches Inhaltsverzeichnis für lange Blog-Posts.
* Extrahiert Überschriften aus dem Content und erstellt Navigation.
*/
export const TableOfContentsBlock: Block = {
slug: 'toc-block',
labels: {
singular: 'Inhaltsverzeichnis',
plural: 'Inhaltsverzeichnisse',
},
imageURL: '/assets/blocks/table-of-contents.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Inhaltsverzeichnis',
},
// Überschriften-Level
{
name: 'levels',
type: 'group',
label: 'Überschriften-Level',
admin: {
description: 'Welche Überschriften-Ebenen einschließen?',
},
fields: [
{
name: 'h2',
type: 'checkbox',
defaultValue: true,
label: 'H2',
},
{
name: 'h3',
type: 'checkbox',
defaultValue: true,
label: 'H3',
},
{
name: 'h4',
type: 'checkbox',
defaultValue: false,
label: 'H4',
},
{
name: 'h5',
type: 'checkbox',
defaultValue: false,
label: 'H5',
},
{
name: 'h6',
type: 'checkbox',
defaultValue: false,
label: 'H6',
},
],
},
// Layout
{
name: 'layout',
type: 'select',
defaultValue: 'list',
label: 'Layout',
options: [
{ label: 'Liste', value: 'list' },
{ label: 'Nummeriert', value: 'numbered' },
{ label: 'Kompakt (Inline)', value: 'inline' },
{ label: 'Sidebar (Sticky)', value: 'sidebar' },
{ label: 'Dropdown/Akkordeon', value: 'dropdown' },
],
},
{
name: 'sidebarPos',
type: 'select',
defaultValue: 'right',
label: 'Sidebar-Position',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
],
admin: {
condition: (_, siblingData) => siblingData?.layout === 'sidebar',
},
},
// Verhalten
{
name: 'behavior',
type: 'group',
label: 'Verhalten',
fields: [
{
name: 'smoothScroll',
type: 'checkbox',
defaultValue: true,
label: 'Sanftes Scrollen',
},
{
name: 'highlightActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiven Abschnitt markieren',
admin: {
description: 'Markiert den aktuell sichtbaren Abschnitt',
},
},
{
name: 'scrollOffset',
type: 'number',
defaultValue: 80,
label: 'Scroll-Offset (px)',
admin: {
description: 'Abstand zum oberen Rand nach dem Scrollen (für Fixed Headers)',
},
},
{
name: 'collapsible',
type: 'checkbox',
defaultValue: false,
label: 'Einklappbar',
admin: {
description: 'User kann das Inhaltsverzeichnis ein-/ausklappen',
},
},
{
name: 'startCollapsed',
type: 'checkbox',
defaultValue: false,
label: 'Anfangs eingeklappt',
admin: {
condition: (_, siblingData) => siblingData?.collapsible,
},
},
{
name: 'showProgress',
type: 'checkbox',
defaultValue: false,
label: 'Lesefortschritt anzeigen',
admin: {
description: 'Fortschrittsbalken oder Prozent-Anzeige',
},
},
{
name: 'progressStyle',
type: 'select',
defaultValue: 'bar',
label: 'Fortschritts-Stil',
options: [
{ label: 'Balken', value: 'bar' },
{ label: 'Prozent', value: 'percent' },
{ label: 'Kreis', value: 'circle' },
],
admin: {
condition: (_, siblingData) => siblingData?.showProgress,
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'light',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
{ label: 'Akzent', value: 'accent' },
],
},
{
name: 'border',
type: 'checkbox',
defaultValue: true,
label: 'Rahmen',
},
{
name: 'borderSide',
type: 'select',
defaultValue: 'left',
label: 'Rahmen-Seite',
options: [
{ label: 'Alle', value: 'all' },
{ label: 'Links', value: 'left' },
{ label: 'Oben', value: 'top' },
],
admin: {
condition: (_, siblingData) => siblingData?.border,
},
},
{
name: 'rounded',
type: 'select',
defaultValue: 'md',
label: 'Ecken',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Klein', value: 'sm' },
{ label: 'Mittel', value: 'md' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'shadow',
type: 'checkbox',
defaultValue: false,
label: 'Schatten',
},
{
name: 'indent',
type: 'checkbox',
defaultValue: true,
label: 'Sub-Überschriften einrücken',
},
{
name: 'showIcon',
type: 'checkbox',
defaultValue: false,
label: 'Icons anzeigen',
admin: {
description: 'Link-Symbol neben Einträgen',
},
},
{
name: 'fontSize',
type: 'select',
defaultValue: 'sm',
label: 'Schriftgröße',
options: [
{ label: 'Klein', value: 'sm' },
{ label: 'Normal', value: 'base' },
{ label: 'Groß', value: 'lg' },
],
},
{
name: 'lineHeight',
type: 'select',
defaultValue: 'relaxed',
label: 'Zeilenhöhe',
options: [
{ label: 'Eng', value: 'tight' },
{ label: 'Normal', value: 'normal' },
{ label: 'Locker', value: 'relaxed' },
],
},
],
},
// Limits
{
name: 'minItems',
type: 'number',
defaultValue: 3,
label: 'Mindestanzahl',
admin: {
description: 'Inhaltsverzeichnis nur anzeigen, wenn mindestens X Einträge',
},
},
{
name: 'maxItems',
type: 'number',
label: 'Maximalanzahl',
admin: {
description: 'Maximale Anzahl angezeigter Einträge (0 = unbegrenzt)',
},
},
// Accessibility
{
name: 'a11yLabel',
type: 'text',
label: 'ARIA Label',
localized: true,
defaultValue: 'Inhaltsverzeichnis',
},
],
}

View file

@ -0,0 +1,381 @@
import type { Block } from 'payload'
/**
* TeamFilterBlock
*
* Interaktive Team-Anzeige mit Filter- und Suchfunktion.
* Ermöglicht Filterung nach Abteilung, Fachgebiet, Sprache.
*/
export const TeamFilterBlock: Block = {
slug: 'team-filter-block',
labels: {
singular: 'Team mit Filter',
plural: 'Team mit Filter',
},
imageURL: '/assets/blocks/team-filter.png',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Unser Team',
},
{
name: 'subtitle',
type: 'textarea',
label: 'Untertitel',
localized: true,
},
// Filter-Optionen
{
name: 'filters',
type: 'group',
label: 'Filter-Optionen',
fields: [
{
name: 'showSearch',
type: 'checkbox',
defaultValue: true,
label: 'Suchfeld anzeigen',
},
{
name: 'searchPlaceholder',
type: 'text',
label: 'Such-Platzhalter',
localized: true,
defaultValue: 'Team durchsuchen...',
admin: {
condition: (_, siblingData) => siblingData?.showSearch,
},
},
{
name: 'showDepartment',
type: 'checkbox',
defaultValue: true,
label: 'Abteilungs-Filter',
},
{
name: 'showSpecialization',
type: 'checkbox',
defaultValue: false,
label: 'Fachgebiets-Filter',
},
{
name: 'showLanguage',
type: 'checkbox',
defaultValue: false,
label: 'Sprach-Filter',
},
{
name: 'showHierarchy',
type: 'checkbox',
defaultValue: false,
label: 'Hierarchie-Filter',
},
{
name: 'filterLayout',
type: 'select',
defaultValue: 'horizontal',
label: 'Filter-Layout',
options: [
{ label: 'Horizontal (nebeneinander)', value: 'horizontal' },
{ label: 'Vertikal (Sidebar)', value: 'sidebar' },
{ label: 'Dropdown-Menü', value: 'dropdown' },
{ label: 'Tabs', value: 'tabs' },
],
},
{
name: 'filterStyle',
type: 'select',
defaultValue: 'buttons',
label: 'Filter-Stil',
options: [
{ label: 'Buttons/Chips', value: 'buttons' },
{ label: 'Dropdown', value: 'select' },
{ label: 'Checkboxen', value: 'checkbox' },
],
admin: {
condition: (_, siblingData) =>
siblingData?.filterLayout !== 'tabs' && siblingData?.filterLayout !== 'dropdown',
},
},
{
name: 'showResultCount',
type: 'checkbox',
defaultValue: true,
label: 'Ergebnis-Anzahl anzeigen',
},
{
name: 'showResetButton',
type: 'checkbox',
defaultValue: true,
label: 'Filter-Reset-Button',
},
],
},
// Anzeige-Optionen
{
name: 'display',
type: 'group',
label: 'Anzeige',
fields: [
{
name: 'layout',
type: 'select',
defaultValue: 'grid',
label: 'Layout',
options: [
{ label: 'Grid', value: 'grid' },
{ label: 'Liste', value: 'list' },
{ label: 'Kompakt', value: 'compact' },
],
},
{
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',
},
},
{
name: 'initialLimit',
type: 'number',
defaultValue: 12,
min: 4,
max: 50,
label: 'Initiale Anzahl',
},
{
name: 'loadMore',
type: 'select',
defaultValue: 'button',
label: 'Mehr laden',
options: [
{ label: '"Mehr laden" Button', value: 'button' },
{ label: 'Infinite Scroll', value: 'infinite' },
{ label: 'Pagination', value: 'pagination' },
{ label: 'Alle anzeigen', value: 'all' },
],
},
{
name: 'loadMoreText',
type: 'text',
label: 'Button-Text',
localized: true,
defaultValue: 'Mehr anzeigen',
admin: {
condition: (_, siblingData) => siblingData?.loadMore === 'button',
},
},
],
},
// Member-Card Optionen
{
name: 'card',
type: 'group',
label: 'Mitglieder-Karte',
fields: [
{
name: 'showImage',
type: 'checkbox',
defaultValue: true,
label: 'Foto anzeigen',
},
{
name: 'imageStyle',
type: 'select',
defaultValue: 'circle',
label: 'Foto-Stil',
options: [
{ label: 'Rund', value: 'circle' },
{ label: 'Abgerundet', value: 'rounded' },
{ label: 'Quadratisch', value: 'square' },
],
admin: {
condition: (_, siblingData) => siblingData?.showImage,
},
},
{
name: 'showRole',
type: 'checkbox',
defaultValue: true,
label: 'Position anzeigen',
},
{
name: 'showDepartment',
type: 'checkbox',
defaultValue: true,
label: 'Abteilung anzeigen',
},
{
name: 'showBio',
type: 'checkbox',
defaultValue: false,
label: 'Kurzbiografie anzeigen',
},
{
name: 'showContact',
type: 'checkbox',
defaultValue: false,
label: 'Kontaktdaten anzeigen',
},
{
name: 'showSocial',
type: 'checkbox',
defaultValue: true,
label: 'Social Links anzeigen',
},
{
name: 'showSpecializations',
type: 'checkbox',
defaultValue: false,
label: 'Fachgebiete als Tags',
},
{
name: 'showLanguages',
type: 'checkbox',
defaultValue: false,
label: 'Sprachen anzeigen',
},
{
name: 'showVCard',
type: 'checkbox',
defaultValue: false,
label: 'vCard-Download anzeigen',
},
{
name: 'linkToProfile',
type: 'checkbox',
defaultValue: true,
label: 'Zur Profilseite verlinken',
},
{
name: 'profileBasePath',
type: 'text',
label: 'Profil-Basis-Pfad',
defaultValue: '/team',
admin: {
condition: (_, siblingData) => siblingData?.linkToProfile,
description: 'z.B. "/team" ergibt "/team/max-mustermann"',
},
},
{
name: 'enableModal',
type: 'checkbox',
defaultValue: false,
label: 'Detail-Modal aktivieren',
admin: {
description: 'Klick öffnet Modal statt Profilseite',
},
},
],
},
// Styling
{
name: 'style',
type: 'group',
label: 'Darstellung',
fields: [
{
name: 'bg',
type: 'select',
defaultValue: 'none',
label: 'Hintergrund',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Hell', value: 'light' },
{ label: 'Dunkel', value: 'dark' },
],
},
{
name: 'cardBg',
type: 'select',
defaultValue: 'white',
label: 'Karten-Hintergrund',
options: [
{ label: 'Weiß', value: 'white' },
{ label: 'Transparent', value: 'transparent' },
{ label: 'Hell', value: 'light' },
],
},
{
name: 'cardShadow',
type: 'checkbox',
defaultValue: true,
label: 'Karten-Schatten',
},
{
name: 'cardHover',
type: 'select',
defaultValue: 'lift',
label: 'Hover-Effekt',
options: [
{ label: 'Keiner', value: 'none' },
{ label: 'Anheben', value: 'lift' },
{ label: 'Schatten verstärken', value: 'shadow' },
{ label: 'Rand hervorheben', value: 'border' },
],
},
{
name: 'gap',
type: 'select',
defaultValue: '24',
label: 'Abstand',
options: [
{ label: 'Klein (16px)', value: '16' },
{ label: 'Normal (24px)', value: '24' },
{ label: 'Groß (32px)', value: '32' },
],
},
{
name: 'animation',
type: 'select',
defaultValue: 'fade',
label: 'Filter-Animation',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Fade', value: 'fade' },
{ label: 'Slide', value: 'slide' },
{ label: 'Scale', value: 'scale' },
],
},
],
},
// Empty State
{
name: 'emptyState',
type: 'group',
label: 'Keine Ergebnisse',
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
defaultValue: 'Keine Mitarbeiter gefunden',
},
{
name: 'message',
type: 'textarea',
label: 'Nachricht',
localized: true,
defaultValue: 'Versuchen Sie andere Filterkriterien.',
},
{
name: 'showResetButton',
type: 'checkbox',
defaultValue: true,
label: 'Filter-Reset anzeigen',
},
],
},
],
}

View file

@ -19,3 +19,13 @@ export { ProcessStepsBlock } from './ProcessStepsBlock'
export { FAQBlock } from './FAQBlock'
export { TeamBlock } from './TeamBlock'
export { ServicesBlock } from './ServicesBlock'
// Blogging Blocks
export { AuthorBioBlock } from './AuthorBioBlock'
export { RelatedPostsBlock } from './RelatedPostsBlock'
export { ShareButtonsBlock } from './ShareButtonsBlock'
export { TableOfContentsBlock } from './TableOfContentsBlock'
// Team Blocks
export { TeamFilterBlock } from './TeamFilterBlock'
export { OrgChartBlock } from './OrgChartBlock'

211
src/collections/Authors.ts Normal file
View file

@ -0,0 +1,211 @@
import type { CollectionConfig } from 'payload'
/**
* Authors Collection
*
* Blog-Autoren mit Bio, Bild und Social Links.
* Kann optional mit Team-Mitgliedern verknüpft werden.
*/
export const Authors: CollectionConfig = {
slug: 'authors',
labels: {
singular: 'Autor',
plural: 'Autoren',
},
admin: {
useAsTitle: 'name',
group: 'Content',
defaultColumns: ['name', 'slug', 'postCount', 'isActive', 'updatedAt'],
description: 'Blog-Autoren und Gastautoren',
},
access: {
read: () => true,
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
fields: [
// Basis-Informationen
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
admin: {
description: 'Anzeigename des Autors',
},
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
description: 'URL-freundlicher Identifier (z.B. "max-mustermann")',
},
},
{
name: 'avatar',
type: 'upload',
relationTo: 'media',
label: 'Profilbild',
admin: {
description: 'Avatar/Profilbild (empfohlen: quadratisch, min. 200x200px)',
},
},
{
name: 'bio',
type: 'richText',
label: 'Biografie',
localized: true,
admin: {
description: 'Ausführliche Biografie für Autorenseite',
},
},
{
name: 'bioShort',
type: 'textarea',
label: 'Kurzbiografie',
localized: true,
admin: {
description: 'Ein bis zwei Sätze für Anzeige unter Artikeln',
},
},
{
name: 'title',
type: 'text',
label: 'Titel/Position',
localized: true,
admin: {
description: 'z.B. "Senior Editor", "Gastautor", "Chefredakteur"',
},
},
// Kontakt & Social
{
name: 'email',
type: 'email',
label: 'E-Mail',
admin: {
description: 'Öffentliche Kontakt-E-Mail (optional)',
},
},
{
name: 'website',
type: 'text',
label: 'Website',
admin: {
description: 'Persönliche Website oder Blog',
},
},
{
name: 'social',
type: 'group',
label: 'Social Media',
fields: [
{
name: 'twitter',
type: 'text',
label: 'Twitter/X',
admin: {
description: 'Twitter-Handle ohne @ (z.B. "maxmustermann")',
},
},
{
name: 'linkedin',
type: 'text',
label: 'LinkedIn',
admin: {
description: 'LinkedIn-Profil-URL oder Username',
},
},
{
name: 'github',
type: 'text',
label: 'GitHub',
admin: {
description: 'GitHub-Username',
},
},
{
name: 'instagram',
type: 'text',
label: 'Instagram',
admin: {
description: 'Instagram-Handle ohne @',
},
},
],
},
// Verknüpfungen
{
name: 'linkedTeam',
type: 'relationship',
relationTo: 'team',
label: 'Team-Mitglied',
admin: {
position: 'sidebar',
description: 'Optional: Verknüpfung mit Team-Eintrag',
},
},
{
name: 'linkedUser',
type: 'relationship',
relationTo: 'users',
label: 'Benutzer',
admin: {
position: 'sidebar',
description: 'Optional: Verknüpfung mit Login-User',
},
},
// Status
{
name: 'isActive',
type: 'checkbox',
defaultValue: true,
label: 'Aktiv',
admin: {
position: 'sidebar',
description: 'Inaktive Autoren erscheinen nicht in Listen',
},
},
{
name: 'isGuest',
type: 'checkbox',
defaultValue: false,
label: 'Gastautor',
admin: {
position: 'sidebar',
description: 'Markierung für Gastautoren',
},
},
{
name: 'featured',
type: 'checkbox',
defaultValue: false,
label: 'Hervorgehoben',
admin: {
position: 'sidebar',
description: 'Für besondere Darstellung auf Autorenseite',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
// Auto-generate slug from name if not provided
if (data && !data.slug && data.name) {
data.slug = data.name
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

View file

@ -20,6 +20,14 @@ import {
FAQBlock,
TeamBlock,
ServicesBlock,
// Blogging Blocks
AuthorBioBlock,
RelatedPostsBlock,
ShareButtonsBlock,
TableOfContentsBlock,
// Team Blocks
TeamFilterBlock,
OrgChartBlock,
} from '../blocks'
import { pagesAccess } from '../lib/access'
@ -94,6 +102,14 @@ export const Pages: CollectionConfig = {
FAQBlock,
TeamBlock,
ServicesBlock,
// Blogging Blocks
AuthorBioBlock,
RelatedPostsBlock,
ShareButtonsBlock,
TableOfContentsBlock,
// Team Blocks
TeamFilterBlock,
OrgChartBlock,
],
},
{

View file

@ -1,6 +1,36 @@
import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'
/**
* Berechnet die geschätzte Lesezeit basierend auf Wortanzahl
* Durchschnitt: ~200 Wörter pro Minute
*/
function calculateReadingTime(content: unknown): number {
if (!content) return 1
// RichText zu Plain Text konvertieren (vereinfacht)
let text = ''
const extractText = (node: unknown): void => {
if (!node || typeof node !== 'object') return
const n = node as Record<string, unknown>
if (n.text && typeof n.text === 'string') {
text += n.text + ' '
}
if (Array.isArray(n.children)) {
n.children.forEach(extractText)
}
if (n.root && typeof n.root === 'object') {
extractText(n.root)
}
}
extractText(content)
const words = text.trim().split(/\s+/).filter(Boolean).length
const minutes = Math.ceil(words / 200)
return Math.max(1, minutes)
}
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
@ -86,11 +116,55 @@ export const Posts: CollectionConfig = {
type: 'relationship',
relationTo: 'categories',
hasMany: true,
label: 'Kategorien',
},
{
name: 'tags',
type: 'relationship',
relationTo: 'tags',
hasMany: true,
label: 'Tags',
admin: {
description: 'Schlagwörter für bessere Auffindbarkeit',
},
},
{
name: 'author',
type: 'text',
type: 'relationship',
relationTo: 'authors',
label: 'Autor',
admin: {
description: 'Hauptautor des Beitrags',
},
},
{
name: 'coAuthors',
type: 'relationship',
relationTo: 'authors',
hasMany: true,
label: 'Co-Autoren',
admin: {
description: 'Weitere beteiligte Autoren',
},
},
{
name: 'authorLegacy',
type: 'text',
label: 'Autor (Legacy)',
admin: {
description: 'Freitext-Autor für ältere Beiträge ohne Autoren-Eintrag',
condition: (_, siblingData) => !siblingData?.author,
},
},
{
name: 'readingTime',
type: 'number',
label: 'Lesezeit (Minuten)',
admin: {
position: 'sidebar',
description: 'Wird automatisch berechnet',
readOnly: true,
},
},
{
name: 'status',
@ -143,4 +217,15 @@ export const Posts: CollectionConfig = {
],
},
],
hooks: {
beforeChange: [
({ data }) => {
// Automatische Lesezeit-Berechnung
if (data?.content) {
data.readingTime = calculateReadingTime(data.content)
}
return data
},
],
},
}

81
src/collections/Tags.ts Normal file
View file

@ -0,0 +1,81 @@
import type { CollectionConfig } from 'payload'
/**
* Tags Collection
*
* Schlagwörter für Blog-Posts und andere Inhalte.
* Flexibler als Kategorien, für freie Verschlagwortung.
*/
export const Tags: CollectionConfig = {
slug: 'tags',
labels: {
singular: 'Tag',
plural: 'Tags',
},
admin: {
useAsTitle: 'name',
group: 'Content',
defaultColumns: ['name', 'slug', 'postCount', 'updatedAt'],
description: 'Schlagwörter für Blog-Posts',
},
access: {
read: () => true,
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
localized: true,
label: 'Name',
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
description: 'URL-freundlicher Identifier (z.B. "javascript")',
},
},
{
name: 'description',
type: 'textarea',
localized: true,
label: 'Beschreibung',
admin: {
description: 'Optionale Beschreibung für Tag-Archivseiten',
},
},
{
name: 'color',
type: 'text',
label: 'Farbe',
admin: {
description: 'Optionale Farbe für Tag-Badge (z.B. "#3B82F6" oder "blue")',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
// Auto-generate slug from name if not provided
if (data && !data.slug && data.name) {
data.slug = data.name
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

View file

@ -34,6 +34,16 @@ export const Team: CollectionConfig = {
description: 'Vollständiger Name',
},
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: {
description: 'URL-freundlicher Identifier (z.B. "max-mustermann")',
},
},
{
name: 'role',
type: 'text',
@ -273,5 +283,61 @@ export const Team: CollectionConfig = {
description: 'Eintrittsdatum (optional)',
},
},
// Hierarchie für Organigramm
{
name: 'reportsTo',
type: 'relationship',
relationTo: 'team',
label: 'Vorgesetzter',
admin: {
position: 'sidebar',
description: 'Direkter Vorgesetzter (für Organigramm)',
},
},
{
name: 'hierarchyLevel',
type: 'select',
label: 'Hierarchie-Ebene',
options: [
{ label: 'Geschäftsführung', value: 'executive' },
{ label: 'Abteilungsleitung', value: 'department_head' },
{ label: 'Teamleitung', value: 'team_lead' },
{ label: 'Mitarbeiter', value: 'employee' },
{ label: 'Praktikant/Azubi', value: 'trainee' },
],
admin: {
position: 'sidebar',
description: 'Für Organigramm-Darstellung',
},
},
// vCard Export erlauben
{
name: 'allowVCard',
type: 'checkbox',
defaultValue: true,
label: 'vCard-Export erlauben',
admin: {
position: 'sidebar',
description: 'Kontaktkarte zum Download anbieten',
},
},
],
hooks: {
beforeChange: [
({ data }) => {
// Auto-generate slug from name if not provided
if (data && !data.slug && data.name) {
data.slug = data.name
.toLowerCase()
.replace(/[äöüß]/g, (match: string) => {
const map: Record<string, string> = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss' }
return map[match] || match
})
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
}
return data
},
],
},
}

View file

@ -0,0 +1,735 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
/**
* Migration: Blogging Collections & Blocks
*
* Erstellt:
* - Tags Collection
* - Authors Collection
* - Posts-Erweiterungen (Tags, Author Relations, Reading Time)
* - AuthorBioBlock, RelatedPostsBlock, ShareButtonsBlock, TableOfContentsBlock
*/
export async function up({ db }: MigrateUpArgs): Promise<void> {
// ========================================
// Tags Collection
// ========================================
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "tags" (
"id" serial PRIMARY KEY NOT NULL,
"slug" varchar NOT NULL UNIQUE,
"color" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS "tags_locales" (
"name" varchar NOT NULL,
"description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL
);
ALTER TABLE "tags_locales"
ADD CONSTRAINT "tags_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action;
CREATE UNIQUE INDEX IF NOT EXISTS "tags_locales_locale_parent_id_unique"
ON "tags_locales" USING btree ("_locale", "_parent_id");
CREATE INDEX IF NOT EXISTS "tags_created_at_idx"
ON "tags" USING btree ("created_at");
`)
// Tags Tenant Column (für Multi-Tenant Plugin)
await db.execute(sql`
ALTER TABLE "tags" ADD COLUMN IF NOT EXISTS "tenant_id" integer;
ALTER TABLE "tags"
ADD CONSTRAINT "tags_tenant_id_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "tags_tenant_idx"
ON "tags" USING btree ("tenant_id");
`)
// ========================================
// Authors Collection
// ========================================
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "authors" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"slug" varchar NOT NULL UNIQUE,
"avatar_id" integer,
"email" varchar,
"website" varchar,
"social_twitter" varchar,
"social_linkedin" varchar,
"social_github" varchar,
"social_instagram" varchar,
"linked_team_id" integer,
"linked_user_id" integer,
"is_active" boolean DEFAULT true,
"is_guest" boolean DEFAULT false,
"featured" boolean DEFAULT false,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE IF NOT EXISTS "authors_locales" (
"bio" jsonb,
"bio_short" varchar,
"title" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" integer NOT NULL
);
ALTER TABLE "authors"
ADD CONSTRAINT "authors_avatar_id_fk"
FOREIGN KEY ("avatar_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "authors"
ADD CONSTRAINT "authors_linked_team_id_fk"
FOREIGN KEY ("linked_team_id") REFERENCES "public"."team"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "authors"
ADD CONSTRAINT "authors_linked_user_id_fk"
FOREIGN KEY ("linked_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "authors_locales"
ADD CONSTRAINT "authors_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action;
CREATE UNIQUE INDEX IF NOT EXISTS "authors_locales_locale_parent_id_unique"
ON "authors_locales" USING btree ("_locale", "_parent_id");
CREATE INDEX IF NOT EXISTS "authors_avatar_idx"
ON "authors" USING btree ("avatar_id");
CREATE INDEX IF NOT EXISTS "authors_created_at_idx"
ON "authors" USING btree ("created_at");
`)
// Authors Tenant Column
await db.execute(sql`
ALTER TABLE "authors" ADD COLUMN IF NOT EXISTS "tenant_id" integer;
ALTER TABLE "authors"
ADD CONSTRAINT "authors_tenant_id_fk"
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "authors_tenant_idx"
ON "authors" USING btree ("tenant_id");
`)
// ========================================
// Posts-Erweiterungen
// ========================================
// Tags Relationship (posts_rels für tags)
await db.execute(sql`
-- Add tags path to posts_rels if needed
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM posts_rels WHERE path = 'tags' LIMIT 1
) THEN
-- tags_id column might not exist yet
ALTER TABLE "posts_rels" ADD COLUMN IF NOT EXISTS "tags_id" integer;
ALTER TABLE "posts_rels"
ADD CONSTRAINT "posts_rels_tags_fk"
FOREIGN KEY ("tags_id") REFERENCES "public"."tags"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "posts_rels_tags_id_idx"
ON "posts_rels" USING btree ("tags_id");
END IF;
END
$$;
`)
// Author Relationship (author_id and co_authors via posts_rels)
await db.execute(sql`
ALTER TABLE "posts" ADD COLUMN IF NOT EXISTS "author_id" integer;
ALTER TABLE "posts" ADD COLUMN IF NOT EXISTS "author_legacy" varchar;
ALTER TABLE "posts" ADD COLUMN IF NOT EXISTS "reading_time" integer;
ALTER TABLE "posts"
ADD CONSTRAINT "posts_author_id_fk"
FOREIGN KEY ("author_id") REFERENCES "public"."authors"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "posts_author_idx"
ON "posts" USING btree ("author_id");
-- Co-Authors via posts_rels
ALTER TABLE "posts_rels" ADD COLUMN IF NOT EXISTS "authors_id" integer;
ALTER TABLE "posts_rels"
ADD CONSTRAINT "posts_rels_authors_fk"
FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "posts_rels_authors_id_idx"
ON "posts_rels" USING btree ("authors_id");
`)
// ========================================
// AuthorBioBlock
// ========================================
await db.execute(sql`
-- Enums for AuthorBioBlock
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_author_bio_source" AS ENUM('post', 'manual');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_author_bio_layout" AS ENUM('card', 'inline', 'compact', 'feature');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_author_bio_show_bio" AS ENUM('none', 'short', 'full');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_author_bio_avatar_size" AS ENUM('sm', 'md', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_author_bio_avatar_shape" AS ENUM('circle', 'square', 'rounded');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_author_bio_bg" AS ENUM('none', 'light', 'dark', 'accent');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "pages_blocks_author_bio_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"source" "enum_pages_blocks_author_bio_source" DEFAULT 'post',
"show_co_authors" boolean DEFAULT true,
"layout" "enum_pages_blocks_author_bio_layout" DEFAULT 'card',
"show_avatar" boolean DEFAULT true,
"show_name" boolean DEFAULT true,
"show_title" boolean DEFAULT true,
"show_bio" "enum_pages_blocks_author_bio_show_bio" DEFAULT 'short',
"show_social" boolean DEFAULT true,
"show_email" boolean DEFAULT false,
"show_website" boolean DEFAULT false,
"show_post_count" boolean DEFAULT false,
"style_avatar_size" "enum_pages_blocks_author_bio_avatar_size" DEFAULT 'md',
"style_avatar_shape" "enum_pages_blocks_author_bio_avatar_shape" DEFAULT 'circle',
"style_bg" "enum_pages_blocks_author_bio_bg" DEFAULT 'light',
"style_border" boolean DEFAULT false,
"style_shadow" boolean DEFAULT false,
"style_divider" boolean DEFAULT true,
"link_to_profile" boolean DEFAULT true,
"block_name" varchar
);
CREATE TABLE IF NOT EXISTS "pages_blocks_author_bio_block_locales" (
"label" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
-- Authors relationship for manual selection
CREATE TABLE IF NOT EXISTS "pages_blocks_author_bio_block_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" varchar NOT NULL,
"path" varchar NOT NULL,
"authors_id" integer
);
ALTER TABLE "pages_blocks_author_bio_block"
ADD CONSTRAINT "pages_blocks_author_bio_block_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_author_bio_block_locales"
ADD CONSTRAINT "pages_blocks_author_bio_block_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_author_bio_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_author_bio_block_rels"
ADD CONSTRAINT "pages_blocks_author_bio_block_rels_parent_fk"
FOREIGN KEY ("parent_id") REFERENCES "public"."pages_blocks_author_bio_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_author_bio_block_rels"
ADD CONSTRAINT "pages_blocks_author_bio_block_rels_authors_fk"
FOREIGN KEY ("authors_id") REFERENCES "public"."authors"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "pages_blocks_author_bio_block_order_idx"
ON "pages_blocks_author_bio_block" USING btree ("_order");
CREATE INDEX IF NOT EXISTS "pages_blocks_author_bio_block_parent_id_idx"
ON "pages_blocks_author_bio_block" USING btree ("_parent_id");
CREATE INDEX IF NOT EXISTS "pages_blocks_author_bio_block_path_idx"
ON "pages_blocks_author_bio_block" USING btree ("_path");
CREATE UNIQUE INDEX IF NOT EXISTS "pages_blocks_author_bio_block_locales_locale_parent_id_unique"
ON "pages_blocks_author_bio_block_locales" USING btree ("_locale", "_parent_id");
`)
// ========================================
// RelatedPostsBlock
// ========================================
await db.execute(sql`
-- Enums for RelatedPostsBlock
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_source" AS ENUM('auto', 'manual', 'category', 'tag', 'latest', 'popular', 'author');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_layout" AS ENUM('grid', 'list', 'slider', 'compact', 'cards');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_cols" AS ENUM('2', '3', '4');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_img_ratio" AS ENUM('square', '4-3', '16-9', '3-4');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_rounded" AS ENUM('none', 'sm', 'md', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_hover" AS ENUM('none', 'lift', 'zoom', 'overlay');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_bg" AS ENUM('none', 'light', 'dark');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_related_posts_gap" AS ENUM('16', '24', '32');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "pages_blocks_related_posts_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"source" "enum_pages_blocks_related_posts_source" DEFAULT 'auto',
"category_id" integer,
"tag_id" integer,
"limit" integer DEFAULT 3,
"exclude_current" boolean DEFAULT true,
"layout" "enum_pages_blocks_related_posts_layout" DEFAULT 'grid',
"cols" "enum_pages_blocks_related_posts_cols" DEFAULT '3',
"show_image" boolean DEFAULT true,
"show_date" boolean DEFAULT true,
"show_author" boolean DEFAULT false,
"show_category" boolean DEFAULT true,
"show_excerpt" boolean DEFAULT true,
"show_reading_time" boolean DEFAULT false,
"show_tags" boolean DEFAULT false,
"style_img_ratio" "enum_pages_blocks_related_posts_img_ratio" DEFAULT '16-9',
"style_rounded" "enum_pages_blocks_related_posts_rounded" DEFAULT 'md',
"style_shadow" boolean DEFAULT false,
"style_hover" "enum_pages_blocks_related_posts_hover" DEFAULT 'lift',
"style_bg" "enum_pages_blocks_related_posts_bg" DEFAULT 'none',
"style_gap" "enum_pages_blocks_related_posts_gap" DEFAULT '24',
"show_all_link" boolean DEFAULT false,
"all_link_url" varchar,
"block_name" varchar
);
CREATE TABLE IF NOT EXISTS "pages_blocks_related_posts_block_locales" (
"title" varchar DEFAULT 'Das könnte Sie auch interessieren',
"all_link_text" varchar DEFAULT 'Alle Artikel anzeigen',
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
-- Posts relationship for manual selection
CREATE TABLE IF NOT EXISTS "pages_blocks_related_posts_block_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" varchar NOT NULL,
"path" varchar NOT NULL,
"posts_id" integer
);
ALTER TABLE "pages_blocks_related_posts_block"
ADD CONSTRAINT "pages_blocks_related_posts_block_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_related_posts_block"
ADD CONSTRAINT "pages_blocks_related_posts_block_category_id_fk"
FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_related_posts_block"
ADD CONSTRAINT "pages_blocks_related_posts_block_tag_id_fk"
FOREIGN KEY ("tag_id") REFERENCES "public"."tags"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_related_posts_block_locales"
ADD CONSTRAINT "pages_blocks_related_posts_block_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_related_posts_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_related_posts_block_rels"
ADD CONSTRAINT "pages_blocks_related_posts_block_rels_parent_fk"
FOREIGN KEY ("parent_id") REFERENCES "public"."pages_blocks_related_posts_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_related_posts_block_rels"
ADD CONSTRAINT "pages_blocks_related_posts_block_rels_posts_fk"
FOREIGN KEY ("posts_id") REFERENCES "public"."posts"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "pages_blocks_related_posts_block_order_idx"
ON "pages_blocks_related_posts_block" USING btree ("_order");
CREATE INDEX IF NOT EXISTS "pages_blocks_related_posts_block_parent_id_idx"
ON "pages_blocks_related_posts_block" USING btree ("_parent_id");
CREATE INDEX IF NOT EXISTS "pages_blocks_related_posts_block_path_idx"
ON "pages_blocks_related_posts_block" USING btree ("_path");
CREATE UNIQUE INDEX IF NOT EXISTS "pages_blocks_related_posts_block_locales_locale_parent_id_unique"
ON "pages_blocks_related_posts_block_locales" USING btree ("_locale", "_parent_id");
`)
// ========================================
// ShareButtonsBlock
// ========================================
await db.execute(sql`
-- Enums for ShareButtonsBlock
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_layout" AS ENUM('horizontal', 'vertical', 'floating', 'sticky');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_align" AS ENUM('left', 'center', 'right');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_float_side" AS ENUM('left', 'right');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_variant" AS ENUM('filled', 'outline', 'icon', 'minimal');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_size" AS ENUM('sm', 'md', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_shape" AS ENUM('square', 'rounded', 'circle', 'pill');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_color" AS ENUM('brand', 'gray', 'dark', 'light', 'accent');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_share_gap" AS ENUM('0', '4', '8', '12');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "pages_blocks_share_buttons_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"platforms_facebook" boolean DEFAULT true,
"platforms_twitter" boolean DEFAULT true,
"platforms_linkedin" boolean DEFAULT true,
"platforms_xing" boolean DEFAULT false,
"platforms_whatsapp" boolean DEFAULT true,
"platforms_telegram" boolean DEFAULT false,
"platforms_email" boolean DEFAULT true,
"platforms_copy" boolean DEFAULT true,
"platforms_print" boolean DEFAULT false,
"platforms_pinterest" boolean DEFAULT false,
"platforms_reddit" boolean DEFAULT false,
"layout" "enum_pages_blocks_share_layout" DEFAULT 'horizontal',
"align" "enum_pages_blocks_share_align" DEFAULT 'left',
"float_side" "enum_pages_blocks_share_float_side" DEFAULT 'left',
"style_variant" "enum_pages_blocks_share_variant" DEFAULT 'filled',
"style_size" "enum_pages_blocks_share_size" DEFAULT 'md',
"style_shape" "enum_pages_blocks_share_shape" DEFAULT 'rounded',
"style_color_scheme" "enum_pages_blocks_share_color" DEFAULT 'brand',
"style_gap" "enum_pages_blocks_share_gap" DEFAULT '8',
"style_show_label" boolean DEFAULT false,
"style_show_count" boolean DEFAULT false,
"behavior_open_in_popup" boolean DEFAULT true,
"behavior_use_native_share" boolean DEFAULT true,
"content_hashtags" varchar,
"content_via" varchar,
"block_name" varchar
);
CREATE TABLE IF NOT EXISTS "pages_blocks_share_buttons_block_locales" (
"label" varchar,
"behavior_copy_feedback" varchar DEFAULT 'Link kopiert!',
"content_custom_title" varchar,
"content_custom_description" varchar,
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
ALTER TABLE "pages_blocks_share_buttons_block"
ADD CONSTRAINT "pages_blocks_share_buttons_block_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_share_buttons_block_locales"
ADD CONSTRAINT "pages_blocks_share_buttons_block_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_share_buttons_block"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "pages_blocks_share_buttons_block_order_idx"
ON "pages_blocks_share_buttons_block" USING btree ("_order");
CREATE INDEX IF NOT EXISTS "pages_blocks_share_buttons_block_parent_id_idx"
ON "pages_blocks_share_buttons_block" USING btree ("_parent_id");
CREATE INDEX IF NOT EXISTS "pages_blocks_share_buttons_block_path_idx"
ON "pages_blocks_share_buttons_block" USING btree ("_path");
CREATE UNIQUE INDEX IF NOT EXISTS "pages_blocks_share_buttons_block_locales_locale_parent_id_unique"
ON "pages_blocks_share_buttons_block_locales" USING btree ("_locale", "_parent_id");
`)
// ========================================
// TableOfContentsBlock
// ========================================
await db.execute(sql`
-- Enums for TableOfContentsBlock
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_layout" AS ENUM('list', 'numbered', 'inline', 'sidebar', 'dropdown');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_sidebar_pos" AS ENUM('left', 'right');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_progress_style" AS ENUM('bar', 'percent', 'circle');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_bg" AS ENUM('none', 'light', 'dark', 'accent');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_border_side" AS ENUM('all', 'left', 'top');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_rounded" AS ENUM('none', 'sm', 'md', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_font_size" AS ENUM('sm', 'base', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_toc_line_height" AS ENUM('tight', 'normal', 'relaxed');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "pages_blocks_toc_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"levels_h2" boolean DEFAULT true,
"levels_h3" boolean DEFAULT true,
"levels_h4" boolean DEFAULT false,
"levels_h5" boolean DEFAULT false,
"levels_h6" boolean DEFAULT false,
"layout" "enum_pages_blocks_toc_layout" DEFAULT 'list',
"sidebar_pos" "enum_pages_blocks_toc_sidebar_pos" DEFAULT 'right',
"behavior_smooth_scroll" boolean DEFAULT true,
"behavior_highlight_active" boolean DEFAULT true,
"behavior_scroll_offset" integer DEFAULT 80,
"behavior_collapsible" boolean DEFAULT false,
"behavior_start_collapsed" boolean DEFAULT false,
"behavior_show_progress" boolean DEFAULT false,
"behavior_progress_style" "enum_pages_blocks_toc_progress_style" DEFAULT 'bar',
"style_bg" "enum_pages_blocks_toc_bg" DEFAULT 'light',
"style_border" boolean DEFAULT true,
"style_border_side" "enum_pages_blocks_toc_border_side" DEFAULT 'left',
"style_rounded" "enum_pages_blocks_toc_rounded" DEFAULT 'md',
"style_shadow" boolean DEFAULT false,
"style_indent" boolean DEFAULT true,
"style_show_icon" boolean DEFAULT false,
"style_font_size" "enum_pages_blocks_toc_font_size" DEFAULT 'sm',
"style_line_height" "enum_pages_blocks_toc_line_height" DEFAULT 'relaxed',
"min_items" integer DEFAULT 3,
"max_items" integer,
"block_name" varchar
);
CREATE TABLE IF NOT EXISTS "pages_blocks_toc_block_locales" (
"title" varchar DEFAULT 'Inhaltsverzeichnis',
"a11y_label" varchar DEFAULT 'Inhaltsverzeichnis',
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
ALTER TABLE "pages_blocks_toc_block"
ADD CONSTRAINT "pages_blocks_toc_block_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_toc_block_locales"
ADD CONSTRAINT "pages_blocks_toc_block_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_toc_block"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "pages_blocks_toc_block_order_idx"
ON "pages_blocks_toc_block" USING btree ("_order");
CREATE INDEX IF NOT EXISTS "pages_blocks_toc_block_parent_id_idx"
ON "pages_blocks_toc_block" USING btree ("_parent_id");
CREATE INDEX IF NOT EXISTS "pages_blocks_toc_block_path_idx"
ON "pages_blocks_toc_block" USING btree ("_path");
CREATE UNIQUE INDEX IF NOT EXISTS "pages_blocks_toc_block_locales_locale_parent_id_unique"
ON "pages_blocks_toc_block_locales" USING btree ("_locale", "_parent_id");
`)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
// Drop Blocks
await db.execute(sql`
DROP TABLE IF EXISTS "pages_blocks_toc_block_locales" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_toc_block" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_share_buttons_block_locales" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_share_buttons_block" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_related_posts_block_rels" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_related_posts_block_locales" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_related_posts_block" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_author_bio_block_rels" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_author_bio_block_locales" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_author_bio_block" CASCADE;
`)
// Drop Block Enums
await db.execute(sql`
DROP TYPE IF EXISTS "enum_pages_blocks_toc_line_height";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_font_size";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_rounded";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_border_side";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_progress_style";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_sidebar_pos";
DROP TYPE IF EXISTS "enum_pages_blocks_toc_layout";
DROP TYPE IF EXISTS "enum_pages_blocks_share_gap";
DROP TYPE IF EXISTS "enum_pages_blocks_share_color";
DROP TYPE IF EXISTS "enum_pages_blocks_share_shape";
DROP TYPE IF EXISTS "enum_pages_blocks_share_size";
DROP TYPE IF EXISTS "enum_pages_blocks_share_variant";
DROP TYPE IF EXISTS "enum_pages_blocks_share_float_side";
DROP TYPE IF EXISTS "enum_pages_blocks_share_align";
DROP TYPE IF EXISTS "enum_pages_blocks_share_layout";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_gap";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_hover";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_rounded";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_img_ratio";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_cols";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_layout";
DROP TYPE IF EXISTS "enum_pages_blocks_related_posts_source";
DROP TYPE IF EXISTS "enum_pages_blocks_author_bio_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_author_bio_avatar_shape";
DROP TYPE IF EXISTS "enum_pages_blocks_author_bio_avatar_size";
DROP TYPE IF EXISTS "enum_pages_blocks_author_bio_show_bio";
DROP TYPE IF EXISTS "enum_pages_blocks_author_bio_layout";
DROP TYPE IF EXISTS "enum_pages_blocks_author_bio_source";
`)
// Drop Posts extensions
await db.execute(sql`
ALTER TABLE "posts" DROP COLUMN IF EXISTS "author_id";
ALTER TABLE "posts" DROP COLUMN IF EXISTS "author_legacy";
ALTER TABLE "posts" DROP COLUMN IF EXISTS "reading_time";
ALTER TABLE "posts_rels" DROP COLUMN IF EXISTS "authors_id";
ALTER TABLE "posts_rels" DROP COLUMN IF EXISTS "tags_id";
`)
// Drop Authors
await db.execute(sql`
DROP TABLE IF EXISTS "authors_locales" CASCADE;
DROP TABLE IF EXISTS "authors" CASCADE;
`)
// Drop Tags
await db.execute(sql`
DROP TABLE IF EXISTS "tags_locales" CASCADE;
DROP TABLE IF EXISTS "tags" CASCADE;
`)
}

View file

@ -0,0 +1,451 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
/**
* Migration: Team Extensions
*
* Erweitert die Team Collection um:
* - Slug für Einzelseiten
* - Hierarchie-Felder (reportsTo, hierarchyLevel) für Organigramm
* - vCard-Export-Flag
*
* Erstellt neue Blocks:
* - TeamFilterBlock
* - OrgChartBlock
*/
export async function up({ db }: MigrateUpArgs): Promise<void> {
// ========================================
// Team Collection Extensions
// ========================================
// Add slug field
await db.execute(sql`
ALTER TABLE "team" ADD COLUMN IF NOT EXISTS "slug" varchar UNIQUE;
CREATE INDEX IF NOT EXISTS "team_slug_idx"
ON "team" USING btree ("slug");
`)
// Add hierarchy fields
await db.execute(sql`
DO $$
BEGIN
CREATE TYPE "enum_team_hierarchy_level" AS ENUM('executive', 'department_head', 'team_lead', 'employee', 'trainee');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
ALTER TABLE "team" ADD COLUMN IF NOT EXISTS "reports_to_id" integer;
ALTER TABLE "team" ADD COLUMN IF NOT EXISTS "hierarchy_level" "enum_team_hierarchy_level";
ALTER TABLE "team" ADD COLUMN IF NOT EXISTS "allow_v_card" boolean DEFAULT true;
ALTER TABLE "team"
ADD CONSTRAINT "team_reports_to_id_fk"
FOREIGN KEY ("reports_to_id") REFERENCES "public"."team"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "team_reports_to_idx"
ON "team" USING btree ("reports_to_id");
CREATE INDEX IF NOT EXISTS "team_hierarchy_level_idx"
ON "team" USING btree ("hierarchy_level");
`)
// ========================================
// TeamFilterBlock
// ========================================
await db.execute(sql`
-- Enums for TeamFilterBlock
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_filter_layout" AS ENUM('horizontal', 'sidebar', 'dropdown', 'tabs');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_filter_style" AS ENUM('buttons', 'select', 'checkbox');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_display_layout" AS ENUM('grid', 'list', 'compact');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_display_cols" AS ENUM('2', '3', '4');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_load_more" AS ENUM('button', 'infinite', 'pagination', 'all');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_card_img_style" AS ENUM('circle', 'rounded', 'square');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_style_bg" AS ENUM('none', 'light', 'dark');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_style_card_bg" AS ENUM('white', 'transparent', 'light');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_style_hover" AS ENUM('none', 'lift', 'shadow', 'border');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_style_gap" AS ENUM('16', '24', '32');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_team_filter_style_anim" AS ENUM('none', 'fade', 'slide', 'scale');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "pages_blocks_team_filter_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
-- Filters
"filters_show_search" boolean DEFAULT true,
"filters_show_department" boolean DEFAULT true,
"filters_show_specialization" boolean DEFAULT false,
"filters_show_language" boolean DEFAULT false,
"filters_show_hierarchy" boolean DEFAULT false,
"filters_filter_layout" "enum_pages_blocks_team_filter_filter_layout" DEFAULT 'horizontal',
"filters_filter_style" "enum_pages_blocks_team_filter_filter_style" DEFAULT 'buttons',
"filters_show_result_count" boolean DEFAULT true,
"filters_show_reset_button" boolean DEFAULT true,
-- Display
"display_layout" "enum_pages_blocks_team_filter_display_layout" DEFAULT 'grid',
"display_columns" "enum_pages_blocks_team_filter_display_cols" DEFAULT '3',
"display_initial_limit" integer DEFAULT 12,
"display_load_more" "enum_pages_blocks_team_filter_load_more" DEFAULT 'button',
-- Card
"card_show_image" boolean DEFAULT true,
"card_image_style" "enum_pages_blocks_team_filter_card_img_style" DEFAULT 'circle',
"card_show_role" boolean DEFAULT true,
"card_show_department" boolean DEFAULT true,
"card_show_bio" boolean DEFAULT false,
"card_show_contact" boolean DEFAULT false,
"card_show_social" boolean DEFAULT true,
"card_show_specializations" boolean DEFAULT false,
"card_show_languages" boolean DEFAULT false,
"card_show_v_card" boolean DEFAULT false,
"card_link_to_profile" boolean DEFAULT true,
"card_profile_base_path" varchar DEFAULT '/team',
"card_enable_modal" boolean DEFAULT false,
-- Style
"style_bg" "enum_pages_blocks_team_filter_style_bg" DEFAULT 'none',
"style_card_bg" "enum_pages_blocks_team_filter_style_card_bg" DEFAULT 'white',
"style_card_shadow" boolean DEFAULT true,
"style_card_hover" "enum_pages_blocks_team_filter_style_hover" DEFAULT 'lift',
"style_gap" "enum_pages_blocks_team_filter_style_gap" DEFAULT '24',
"style_animation" "enum_pages_blocks_team_filter_style_anim" DEFAULT 'fade',
-- Empty state
"empty_state_show_reset_button" boolean DEFAULT true,
"block_name" varchar
);
CREATE TABLE IF NOT EXISTS "pages_blocks_team_filter_block_locales" (
"title" varchar DEFAULT 'Unser Team',
"subtitle" varchar,
"filters_search_placeholder" varchar DEFAULT 'Team durchsuchen...',
"display_load_more_text" varchar DEFAULT 'Mehr anzeigen',
"empty_state_title" varchar DEFAULT 'Keine Mitarbeiter gefunden',
"empty_state_message" varchar DEFAULT 'Versuchen Sie andere Filterkriterien.',
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
ALTER TABLE "pages_blocks_team_filter_block"
ADD CONSTRAINT "pages_blocks_team_filter_block_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_team_filter_block_locales"
ADD CONSTRAINT "pages_blocks_team_filter_block_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_team_filter_block"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "pages_blocks_team_filter_block_order_idx"
ON "pages_blocks_team_filter_block" USING btree ("_order");
CREATE INDEX IF NOT EXISTS "pages_blocks_team_filter_block_parent_id_idx"
ON "pages_blocks_team_filter_block" USING btree ("_parent_id");
CREATE INDEX IF NOT EXISTS "pages_blocks_team_filter_block_path_idx"
ON "pages_blocks_team_filter_block" USING btree ("_path");
CREATE UNIQUE INDEX IF NOT EXISTS "pages_blocks_team_filter_block_locales_locale_parent_id_unique"
ON "pages_blocks_team_filter_block_locales" USING btree ("_locale", "_parent_id");
`)
// ========================================
// OrgChartBlock
// ========================================
await db.execute(sql`
-- Enums for OrgChartBlock
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_source" AS ENUM('auto', 'department', 'manual');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_layout" AS ENUM('tree', 'tree-horizontal', 'org', 'radial', 'layers');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_direction" AS ENUM('top', 'bottom', 'left', 'right');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_node_style" AS ENUM('card', 'compact', 'avatar', 'text');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_node_img_size" AS ENUM('sm', 'md', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_node_img_shape" AS ENUM('circle', 'rounded', 'square');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_node_click" AS ENUM('none', 'modal', 'link', 'expand');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_conn_style" AS ENUM('straight', 'angular', 'curved', 'dashed');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_conn_color" AS ENUM('gray', 'dark', 'accent', 'gradient');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_conn_thick" AS ENUM('1', '2', '3');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_style_bg" AS ENUM('none', 'light', 'white', 'dark', 'gradient');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_style_node_bg" AS ENUM('white', 'light', 'transparent', 'accent');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_style_spacing" AS ENUM('sm', 'md', 'lg');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
DO $$
BEGIN
CREATE TYPE "enum_pages_blocks_org_chart_style_height" AS ENUM('auto', '300', '400', '500', '600', 'full');
EXCEPTION WHEN duplicate_object THEN null;
END $$;
`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "pages_blocks_org_chart_block" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"_path" text NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
-- Source
"source" "enum_pages_blocks_org_chart_source" DEFAULT 'auto',
"root_member_id" integer,
"department" varchar,
"max_depth" integer DEFAULT 5,
-- Layout
"layout" "enum_pages_blocks_org_chart_layout" DEFAULT 'tree',
"direction" "enum_pages_blocks_org_chart_direction" DEFAULT 'top',
-- Node
"node_style" "enum_pages_blocks_org_chart_node_style" DEFAULT 'card',
"node_show_image" boolean DEFAULT true,
"node_image_size" "enum_pages_blocks_org_chart_node_img_size" DEFAULT 'md',
"node_image_shape" "enum_pages_blocks_org_chart_node_img_shape" DEFAULT 'circle',
"node_show_name" boolean DEFAULT true,
"node_show_role" boolean DEFAULT true,
"node_show_department" boolean DEFAULT false,
"node_show_contact" boolean DEFAULT false,
"node_click_action" "enum_pages_blocks_org_chart_node_click" DEFAULT 'modal',
"node_profile_base_path" varchar DEFAULT '/team',
-- Connectors
"connectors_style" "enum_pages_blocks_org_chart_conn_style" DEFAULT 'straight',
"connectors_color" "enum_pages_blocks_org_chart_conn_color" DEFAULT 'gray',
"connectors_thickness" "enum_pages_blocks_org_chart_conn_thick" DEFAULT '2',
"connectors_animated" boolean DEFAULT false,
-- Levels
"levels_color_by_level" boolean DEFAULT true,
"levels_size_by_level" boolean DEFAULT true,
"levels_collapsible" boolean DEFAULT true,
"levels_initially_expanded" integer DEFAULT 2,
-- Interaction
"interaction_zoomable" boolean DEFAULT true,
"interaction_pannable" boolean DEFAULT true,
"interaction_minimap" boolean DEFAULT false,
"interaction_search" boolean DEFAULT false,
"interaction_highlight" boolean DEFAULT true,
"interaction_fullscreen" boolean DEFAULT true,
"interaction_export" boolean DEFAULT false,
-- Style
"style_bg" "enum_pages_blocks_org_chart_style_bg" DEFAULT 'light',
"style_node_bg" "enum_pages_blocks_org_chart_style_node_bg" DEFAULT 'white',
"style_node_shadow" boolean DEFAULT true,
"style_node_border" boolean DEFAULT false,
"style_spacing" "enum_pages_blocks_org_chart_style_spacing" DEFAULT 'md',
"style_min_height" "enum_pages_blocks_org_chart_style_height" DEFAULT '400',
-- Other
"show_legend" boolean DEFAULT false,
"block_name" varchar
);
CREATE TABLE IF NOT EXISTS "pages_blocks_org_chart_block_locales" (
"title" varchar DEFAULT 'Unsere Struktur',
"subtitle" varchar,
"a11y_label" varchar DEFAULT 'Organigramm der Unternehmensstruktur',
"id" serial PRIMARY KEY NOT NULL,
"_locale" "_locales" NOT NULL,
"_parent_id" varchar NOT NULL
);
-- Selected members relationship
CREATE TABLE IF NOT EXISTS "pages_blocks_org_chart_block_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" varchar NOT NULL,
"path" varchar NOT NULL,
"team_id" integer
);
ALTER TABLE "pages_blocks_org_chart_block"
ADD CONSTRAINT "pages_blocks_org_chart_block_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_org_chart_block"
ADD CONSTRAINT "pages_blocks_org_chart_block_root_member_id_fk"
FOREIGN KEY ("root_member_id") REFERENCES "public"."team"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "pages_blocks_org_chart_block_locales"
ADD CONSTRAINT "pages_blocks_org_chart_block_locales_parent_id_fk"
FOREIGN KEY ("_parent_id") REFERENCES "public"."pages_blocks_org_chart_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_org_chart_block_rels"
ADD CONSTRAINT "pages_blocks_org_chart_block_rels_parent_fk"
FOREIGN KEY ("parent_id") REFERENCES "public"."pages_blocks_org_chart_block"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "pages_blocks_org_chart_block_rels"
ADD CONSTRAINT "pages_blocks_org_chart_block_rels_team_fk"
FOREIGN KEY ("team_id") REFERENCES "public"."team"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX IF NOT EXISTS "pages_blocks_org_chart_block_order_idx"
ON "pages_blocks_org_chart_block" USING btree ("_order");
CREATE INDEX IF NOT EXISTS "pages_blocks_org_chart_block_parent_id_idx"
ON "pages_blocks_org_chart_block" USING btree ("_parent_id");
CREATE INDEX IF NOT EXISTS "pages_blocks_org_chart_block_path_idx"
ON "pages_blocks_org_chart_block" USING btree ("_path");
CREATE UNIQUE INDEX IF NOT EXISTS "pages_blocks_org_chart_block_locales_locale_parent_id_unique"
ON "pages_blocks_org_chart_block_locales" USING btree ("_locale", "_parent_id");
`)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
// Drop OrgChartBlock
await db.execute(sql`
DROP TABLE IF EXISTS "pages_blocks_org_chart_block_rels" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_org_chart_block_locales" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_org_chart_block" CASCADE;
`)
// Drop OrgChartBlock enums
await db.execute(sql`
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_style_height";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_style_spacing";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_style_node_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_style_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_conn_thick";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_conn_color";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_conn_style";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_node_click";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_node_img_shape";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_node_img_size";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_node_style";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_direction";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_layout";
DROP TYPE IF EXISTS "enum_pages_blocks_org_chart_source";
`)
// Drop TeamFilterBlock
await db.execute(sql`
DROP TABLE IF EXISTS "pages_blocks_team_filter_block_locales" CASCADE;
DROP TABLE IF EXISTS "pages_blocks_team_filter_block" CASCADE;
`)
// Drop TeamFilterBlock enums
await db.execute(sql`
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_style_anim";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_style_gap";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_style_hover";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_style_card_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_style_bg";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_card_img_style";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_load_more";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_display_cols";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_display_layout";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_filter_style";
DROP TYPE IF EXISTS "enum_pages_blocks_team_filter_filter_layout";
`)
// Drop Team extensions
await db.execute(sql`
ALTER TABLE "team" DROP COLUMN IF EXISTS "slug";
ALTER TABLE "team" DROP COLUMN IF EXISTS "reports_to_id";
ALTER TABLE "team" DROP COLUMN IF EXISTS "hierarchy_level";
ALTER TABLE "team" DROP COLUMN IF EXISTS "allow_v_card";
DROP TYPE IF EXISTS "enum_team_hierarchy_level";
`)
}

View file

@ -86,6 +86,8 @@ export interface Config {
products: Product;
timelines: Timeline;
workflows: Workflow;
tags: Tag;
authors: Author;
'cookie-configurations': CookieConfiguration;
'cookie-inventory': CookieInventory;
'consent-logs': ConsentLog;
@ -121,6 +123,8 @@ export interface Config {
products: ProductsSelect<false> | ProductsSelect<true>;
timelines: TimelinesSelect<false> | TimelinesSelect<true>;
workflows: WorkflowsSelect<false> | WorkflowsSelect<true>;
tags: TagsSelect<false> | TagsSelect<true>;
authors: AuthorsSelect<false> | AuthorsSelect<true>;
'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>;
'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>;
'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>;
@ -1101,6 +1105,377 @@ export interface Page {
blockName?: string | null;
blockType: 'services-block';
}
| {
/**
* Woher sollen die Autoren-Daten kommen?
*/
source?: ('post' | 'manual') | null;
/**
* Wählen Sie die anzuzeigenden Autoren
*/
authors?: (number | Author)[] | null;
/**
* Auch Co-Autoren des Artikels anzeigen
*/
showCoAuthors?: boolean | null;
layout?: ('card' | 'inline' | 'compact' | 'feature') | null;
show?: {
avatar?: boolean | null;
name?: boolean | null;
title?: boolean | null;
bio?: ('none' | 'short' | 'full') | null;
social?: boolean | null;
email?: boolean | null;
website?: boolean | null;
postCount?: boolean | null;
};
style?: {
avatarSize?: ('sm' | 'md' | 'lg') | null;
avatarShape?: ('circle' | 'square' | 'rounded') | null;
bg?: ('none' | 'light' | 'dark' | 'accent') | null;
border?: boolean | null;
shadow?: boolean | null;
/**
* Linie zur Trennung vom Artikel-Inhalt
*/
divider?: boolean | null;
};
/**
* z.B. "Über den Autor", "Geschrieben von" (optional)
*/
label?: string | null;
linkToProfile?: boolean | null;
id?: string | null;
blockName?: string | null;
blockType: 'author-bio-block';
}
| {
title?: string | null;
source?: ('auto' | 'manual' | 'category' | 'tag' | 'latest' | 'popular' | 'author') | null;
/**
* Wählen Sie die anzuzeigenden Artikel
*/
posts?: (number | Post)[] | null;
category?: (number | null) | Category;
tag?: (number | null) | Tag;
limit?: number | null;
/**
* Den Artikel, auf dem dieser Block ist, nicht anzeigen
*/
excludeCurrent?: boolean | null;
layout?: ('grid' | 'list' | 'slider' | 'compact' | 'cards') | null;
cols?: ('2' | '3' | '4') | null;
show?: {
image?: boolean | null;
date?: boolean | null;
author?: boolean | null;
category?: boolean | null;
excerpt?: boolean | null;
readingTime?: boolean | null;
tags?: boolean | null;
};
style?: {
imgRatio?: ('square' | '4-3' | '16-9' | '3-4') | null;
rounded?: ('none' | 'sm' | 'md' | 'lg') | null;
shadow?: boolean | null;
hover?: ('none' | 'lift' | 'zoom' | 'overlay') | null;
bg?: ('none' | 'light' | 'dark') | null;
gap?: ('16' | '24' | '32') | null;
};
showAllLink?: boolean | null;
allLinkText?: string | null;
/**
* z.B. /blog oder /news
*/
allLinkUrl?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'related-posts-block';
}
| {
/**
* z.B. "Teilen:", "Artikel teilen" (optional)
*/
label?: string | null;
platforms?: {
facebook?: boolean | null;
twitter?: boolean | null;
linkedin?: boolean | null;
xing?: boolean | null;
whatsapp?: boolean | null;
telegram?: boolean | null;
email?: boolean | null;
copy?: boolean | null;
print?: boolean | null;
pinterest?: boolean | null;
reddit?: boolean | null;
};
layout?: ('horizontal' | 'vertical' | 'floating' | 'sticky') | null;
align?: ('left' | 'center' | 'right') | null;
floatSide?: ('left' | 'right') | null;
style?: {
variant?: ('filled' | 'outline' | 'icon' | 'minimal') | null;
size?: ('sm' | 'md' | 'lg') | null;
shape?: ('square' | 'rounded' | 'circle' | 'pill') | null;
colorScheme?: ('brand' | 'gray' | 'dark' | 'light' | 'accent') | null;
gap?: ('0' | '4' | '8' | '12') | null;
showLabel?: boolean | null;
/**
* Zeigt Anzahl der Shares (sofern verfügbar)
*/
showCount?: boolean | null;
};
behavior?: {
/**
* Share-Dialoge in kleinem Popup statt neuem Tab
*/
openInPopup?: boolean | null;
/**
* Auf mobilen Geräten den System-Share-Dialog verwenden
*/
useNativeShare?: boolean | null;
/**
* Text der angezeigt wird nach dem Kopieren
*/
copyFeedback?: string | null;
};
/**
* Überschreibt automatische Werte (optional)
*/
content?: {
/**
* Leer lassen für automatischen Seitentitel
*/
customTitle?: string | null;
/**
* Leer lassen für automatische Meta-Description
*/
customDescription?: string | null;
/**
* Komma-getrennt, ohne # (z.B. "blog,news,tech")
*/
hashtags?: string | null;
/**
* Twitter-Handle ohne @ (z.B. "c2sgmbh")
*/
via?: string | null;
};
id?: string | null;
blockName?: string | null;
blockType: 'share-buttons-block';
}
| {
title?: string | null;
/**
* Welche Überschriften-Ebenen einschließen?
*/
levels?: {
h2?: boolean | null;
h3?: boolean | null;
h4?: boolean | null;
h5?: boolean | null;
h6?: boolean | null;
};
layout?: ('list' | 'numbered' | 'inline' | 'sidebar' | 'dropdown') | null;
sidebarPos?: ('left' | 'right') | null;
behavior?: {
smoothScroll?: boolean | null;
/**
* Markiert den aktuell sichtbaren Abschnitt
*/
highlightActive?: boolean | null;
/**
* Abstand zum oberen Rand nach dem Scrollen (für Fixed Headers)
*/
scrollOffset?: number | null;
/**
* User kann das Inhaltsverzeichnis ein-/ausklappen
*/
collapsible?: boolean | null;
startCollapsed?: boolean | null;
/**
* Fortschrittsbalken oder Prozent-Anzeige
*/
showProgress?: boolean | null;
progressStyle?: ('bar' | 'percent' | 'circle') | null;
};
style?: {
bg?: ('none' | 'light' | 'dark' | 'accent') | null;
border?: boolean | null;
borderSide?: ('all' | 'left' | 'top') | null;
rounded?: ('none' | 'sm' | 'md' | 'lg') | null;
shadow?: boolean | null;
indent?: boolean | null;
/**
* Link-Symbol neben Einträgen
*/
showIcon?: boolean | null;
fontSize?: ('sm' | 'base' | 'lg') | null;
lineHeight?: ('tight' | 'normal' | 'relaxed') | null;
};
/**
* Inhaltsverzeichnis nur anzeigen, wenn mindestens X Einträge
*/
minItems?: number | null;
/**
* Maximale Anzahl angezeigter Einträge (0 = unbegrenzt)
*/
maxItems?: number | null;
a11yLabel?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'toc-block';
}
| {
title?: string | null;
subtitle?: string | null;
filters?: {
showSearch?: boolean | null;
searchPlaceholder?: string | null;
showDepartment?: boolean | null;
showSpecialization?: boolean | null;
showLanguage?: boolean | null;
showHierarchy?: boolean | null;
filterLayout?: ('horizontal' | 'sidebar' | 'dropdown' | 'tabs') | null;
filterStyle?: ('buttons' | 'select' | 'checkbox') | null;
showResultCount?: boolean | null;
showResetButton?: boolean | null;
};
display?: {
layout?: ('grid' | 'list' | 'compact') | null;
columns?: ('2' | '3' | '4') | null;
initialLimit?: number | null;
loadMore?: ('button' | 'infinite' | 'pagination' | 'all') | null;
loadMoreText?: string | null;
};
card?: {
showImage?: boolean | null;
imageStyle?: ('circle' | 'rounded' | 'square') | null;
showRole?: boolean | null;
showDepartment?: boolean | null;
showBio?: boolean | null;
showContact?: boolean | null;
showSocial?: boolean | null;
showSpecializations?: boolean | null;
showLanguages?: boolean | null;
showVCard?: boolean | null;
linkToProfile?: boolean | null;
/**
* z.B. "/team" ergibt "/team/max-mustermann"
*/
profileBasePath?: string | null;
/**
* Klick öffnet Modal statt Profilseite
*/
enableModal?: boolean | null;
};
style?: {
bg?: ('none' | 'light' | 'dark') | null;
cardBg?: ('white' | 'transparent' | 'light') | null;
cardShadow?: boolean | null;
cardHover?: ('none' | 'lift' | 'shadow' | 'border') | null;
gap?: ('16' | '24' | '32') | null;
animation?: ('none' | 'fade' | 'slide' | 'scale') | null;
};
emptyState?: {
title?: string | null;
message?: string | null;
showResetButton?: boolean | null;
};
id?: string | null;
blockName?: string | null;
blockType: 'team-filter-block';
}
| {
title?: string | null;
subtitle?: string | null;
source?: ('auto' | 'department' | 'manual') | null;
/**
* Oberstes Element (z.B. Geschäftsführer). Leer = automatisch ermitteln.
*/
rootMember?: (number | null) | Team;
/**
* Zeigt nur diese Abteilung
*/
department?: string | null;
selectedMembers?: (number | Team)[] | null;
/**
* Wie viele Hierarchie-Ebenen anzeigen
*/
maxDepth?: number | null;
layout?: ('tree' | 'tree-horizontal' | 'org' | 'radial' | 'layers') | null;
direction?: ('top' | 'bottom' | 'left' | 'right') | null;
node?: {
style?: ('card' | 'compact' | 'avatar' | 'text') | null;
showImage?: boolean | null;
imageSize?: ('sm' | 'md' | 'lg') | null;
imageShape?: ('circle' | 'rounded' | 'square') | null;
showName?: boolean | null;
showRole?: boolean | null;
showDepartment?: boolean | null;
showContact?: boolean | null;
clickAction?: ('none' | 'modal' | 'link' | 'expand') | null;
profileBasePath?: string | null;
};
connectors?: {
style?: ('straight' | 'angular' | 'curved' | 'dashed') | null;
color?: ('gray' | 'dark' | 'accent' | 'gradient') | null;
thickness?: ('1' | '2' | '3') | null;
/**
* Animierte Linien beim Laden
*/
animated?: boolean | null;
};
levels?: {
/**
* Verschiedene Farben pro Hierarchie-Ebene
*/
colorByLevel?: boolean | null;
/**
* Höhere Ebenen größer darstellen
*/
sizeByLevel?: boolean | null;
collapsible?: boolean | null;
/**
* Wie viele Ebenen anfangs sichtbar
*/
initiallyExpanded?: number | null;
};
interaction?: {
zoomable?: boolean | null;
pannable?: boolean | null;
/**
* Kleine Übersichtskarte bei großen Organigrammen
*/
minimap?: boolean | null;
search?: boolean | null;
/**
* Bei Hover Pfad zur Wurzel hervorheben
*/
highlight?: boolean | null;
fullscreen?: boolean | null;
/**
* Als PNG oder PDF exportieren
*/
export?: boolean | null;
};
style?: {
bg?: ('none' | 'light' | 'white' | 'dark' | 'gradient') | null;
nodeBg?: ('white' | 'light' | 'transparent' | 'accent') | null;
nodeShadow?: boolean | null;
nodeBorder?: boolean | null;
spacing?: ('sm' | 'md' | 'lg') | null;
minHeight?: ('auto' | '300' | '400' | '500' | '600' | 'full') | null;
};
/**
* Erklärt Farben und Symbole
*/
showLegend?: boolean | null;
a11yLabel?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'org-chart-block';
}
)[]
| null;
seo?: {
@ -1248,6 +1623,10 @@ export interface Team {
* Vollständiger Name
*/
name: string;
/**
* URL-freundlicher Identifier (z.B. "max-mustermann")
*/
slug: string;
/**
* z.B. "Geschäftsführer", "Pflegedienstleitung", "Fotograf"
*/
@ -1342,6 +1721,18 @@ export interface Team {
* Eintrittsdatum (optional)
*/
startDate?: string | null;
/**
* Direkter Vorgesetzter (für Organigramm)
*/
reportsTo?: (number | null) | Team;
/**
* Für Organigramm-Darstellung
*/
hierarchyLevel?: ('executive' | 'department_head' | 'team_lead' | 'employee' | 'trainee') | null;
/**
* Kontaktkarte zum Download anbieten
*/
allowVCard?: boolean | null;
updatedAt: string;
createdAt: string;
}
@ -1582,6 +1973,102 @@ export interface Service {
updatedAt: string;
createdAt: string;
}
/**
* Blog-Autoren und Gastautoren
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "authors".
*/
export interface Author {
id: number;
tenant?: (number | null) | Tenant;
/**
* Anzeigename des Autors
*/
name: string;
/**
* URL-freundlicher Identifier (z.B. "max-mustermann")
*/
slug: string;
/**
* Avatar/Profilbild (empfohlen: quadratisch, min. 200x200px)
*/
avatar?: (number | null) | Media;
/**
* Ausführliche Biografie für Autorenseite
*/
bio?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* Ein bis zwei Sätze für Anzeige unter Artikeln
*/
bioShort?: string | null;
/**
* z.B. "Senior Editor", "Gastautor", "Chefredakteur"
*/
title?: string | null;
/**
* Öffentliche Kontakt-E-Mail (optional)
*/
email?: string | null;
/**
* Persönliche Website oder Blog
*/
website?: string | null;
social?: {
/**
* Twitter-Handle ohne @ (z.B. "maxmustermann")
*/
twitter?: string | null;
/**
* LinkedIn-Profil-URL oder Username
*/
linkedin?: string | null;
/**
* GitHub-Username
*/
github?: string | null;
/**
* Instagram-Handle ohne @
*/
instagram?: string | null;
};
/**
* Optional: Verknüpfung mit Team-Eintrag
*/
linkedTeam?: (number | null) | Team;
/**
* Optional: Verknüpfung mit Login-User
*/
linkedUser?: (number | null) | User;
/**
* Inaktive Autoren erscheinen nicht in Listen
*/
isActive?: boolean | null;
/**
* Markierung für Gastautoren
*/
isGuest?: boolean | null;
/**
* Für besondere Darstellung auf Autorenseite
*/
featured?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
@ -1623,7 +2110,26 @@ export interface Post {
[k: string]: unknown;
};
categories?: (number | Category)[] | null;
author?: string | null;
/**
* Schlagwörter für bessere Auffindbarkeit
*/
tags?: (number | Tag)[] | null;
/**
* Hauptautor des Beitrags
*/
author?: (number | null) | Author;
/**
* Weitere beteiligte Autoren
*/
coAuthors?: (number | Author)[] | null;
/**
* Freitext-Autor für ältere Beiträge ohne Autoren-Eintrag
*/
authorLegacy?: string | null;
/**
* Wird automatisch berechnet
*/
readingTime?: number | null;
status?: ('draft' | 'published' | 'archived') | null;
publishedAt?: string | null;
seo?: {
@ -1634,6 +2140,31 @@ export interface Post {
updatedAt: string;
createdAt: string;
}
/**
* Schlagwörter für Blog-Posts
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags".
*/
export interface Tag {
id: number;
tenant?: (number | null) | Tenant;
name: string;
/**
* URL-freundlicher Identifier (z.B. "javascript")
*/
slug: string;
/**
* Optionale Beschreibung für Tag-Archivseiten
*/
description?: string | null;
/**
* Optionale Farbe für Tag-Badge (z.B. "#3B82F6" oder "blue")
*/
color?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "social-links".
@ -3102,6 +3633,14 @@ export interface PayloadLockedDocument {
relationTo: 'workflows';
value: number | Workflow;
} | null)
| ({
relationTo: 'tags';
value: number | Tag;
} | null)
| ({
relationTo: 'authors';
value: number | Author;
} | null)
| ({
relationTo: 'cookie-configurations';
value: number | CookieConfiguration;
@ -3951,6 +4490,308 @@ export interface PagesSelect<T extends boolean = true> {
id?: T;
blockName?: T;
};
'author-bio-block'?:
| T
| {
source?: T;
authors?: T;
showCoAuthors?: T;
layout?: T;
show?:
| T
| {
avatar?: T;
name?: T;
title?: T;
bio?: T;
social?: T;
email?: T;
website?: T;
postCount?: T;
};
style?:
| T
| {
avatarSize?: T;
avatarShape?: T;
bg?: T;
border?: T;
shadow?: T;
divider?: T;
};
label?: T;
linkToProfile?: T;
id?: T;
blockName?: T;
};
'related-posts-block'?:
| T
| {
title?: T;
source?: T;
posts?: T;
category?: T;
tag?: T;
limit?: T;
excludeCurrent?: T;
layout?: T;
cols?: T;
show?:
| T
| {
image?: T;
date?: T;
author?: T;
category?: T;
excerpt?: T;
readingTime?: T;
tags?: T;
};
style?:
| T
| {
imgRatio?: T;
rounded?: T;
shadow?: T;
hover?: T;
bg?: T;
gap?: T;
};
showAllLink?: T;
allLinkText?: T;
allLinkUrl?: T;
id?: T;
blockName?: T;
};
'share-buttons-block'?:
| T
| {
label?: T;
platforms?:
| T
| {
facebook?: T;
twitter?: T;
linkedin?: T;
xing?: T;
whatsapp?: T;
telegram?: T;
email?: T;
copy?: T;
print?: T;
pinterest?: T;
reddit?: T;
};
layout?: T;
align?: T;
floatSide?: T;
style?:
| T
| {
variant?: T;
size?: T;
shape?: T;
colorScheme?: T;
gap?: T;
showLabel?: T;
showCount?: T;
};
behavior?:
| T
| {
openInPopup?: T;
useNativeShare?: T;
copyFeedback?: T;
};
content?:
| T
| {
customTitle?: T;
customDescription?: T;
hashtags?: T;
via?: T;
};
id?: T;
blockName?: T;
};
'toc-block'?:
| T
| {
title?: T;
levels?:
| T
| {
h2?: T;
h3?: T;
h4?: T;
h5?: T;
h6?: T;
};
layout?: T;
sidebarPos?: T;
behavior?:
| T
| {
smoothScroll?: T;
highlightActive?: T;
scrollOffset?: T;
collapsible?: T;
startCollapsed?: T;
showProgress?: T;
progressStyle?: T;
};
style?:
| T
| {
bg?: T;
border?: T;
borderSide?: T;
rounded?: T;
shadow?: T;
indent?: T;
showIcon?: T;
fontSize?: T;
lineHeight?: T;
};
minItems?: T;
maxItems?: T;
a11yLabel?: T;
id?: T;
blockName?: T;
};
'team-filter-block'?:
| T
| {
title?: T;
subtitle?: T;
filters?:
| T
| {
showSearch?: T;
searchPlaceholder?: T;
showDepartment?: T;
showSpecialization?: T;
showLanguage?: T;
showHierarchy?: T;
filterLayout?: T;
filterStyle?: T;
showResultCount?: T;
showResetButton?: T;
};
display?:
| T
| {
layout?: T;
columns?: T;
initialLimit?: T;
loadMore?: T;
loadMoreText?: T;
};
card?:
| T
| {
showImage?: T;
imageStyle?: T;
showRole?: T;
showDepartment?: T;
showBio?: T;
showContact?: T;
showSocial?: T;
showSpecializations?: T;
showLanguages?: T;
showVCard?: T;
linkToProfile?: T;
profileBasePath?: T;
enableModal?: T;
};
style?:
| T
| {
bg?: T;
cardBg?: T;
cardShadow?: T;
cardHover?: T;
gap?: T;
animation?: T;
};
emptyState?:
| T
| {
title?: T;
message?: T;
showResetButton?: T;
};
id?: T;
blockName?: T;
};
'org-chart-block'?:
| T
| {
title?: T;
subtitle?: T;
source?: T;
rootMember?: T;
department?: T;
selectedMembers?: T;
maxDepth?: T;
layout?: T;
direction?: T;
node?:
| T
| {
style?: T;
showImage?: T;
imageSize?: T;
imageShape?: T;
showName?: T;
showRole?: T;
showDepartment?: T;
showContact?: T;
clickAction?: T;
profileBasePath?: T;
};
connectors?:
| T
| {
style?: T;
color?: T;
thickness?: T;
animated?: T;
};
levels?:
| T
| {
colorByLevel?: T;
sizeByLevel?: T;
collapsible?: T;
initiallyExpanded?: T;
};
interaction?:
| T
| {
zoomable?: T;
pannable?: T;
minimap?: T;
search?: T;
highlight?: T;
fullscreen?: T;
export?: T;
};
style?:
| T
| {
bg?: T;
nodeBg?: T;
nodeShadow?: T;
nodeBorder?: T;
spacing?: T;
minHeight?: T;
};
showLegend?: T;
a11yLabel?: T;
id?: T;
blockName?: T;
};
};
seo?:
| T
@ -3978,7 +4819,11 @@ export interface PostsSelect<T extends boolean = true> {
featuredImage?: T;
content?: T;
categories?: T;
tags?: T;
author?: T;
coAuthors?: T;
authorLegacy?: T;
readingTime?: T;
status?: T;
publishedAt?: T;
seo?:
@ -4060,6 +4905,7 @@ export interface FaqsSelect<T extends boolean = true> {
export interface TeamSelect<T extends boolean = true> {
tenant?: T;
name?: T;
slug?: T;
role?: T;
department?: T;
image?: T;
@ -4101,6 +4947,9 @@ export interface TeamSelect<T extends boolean = true> {
isFeatured?: T;
order?: T;
startDate?: T;
reportsTo?: T;
hierarchyLevel?: T;
allowVCard?: T;
updatedAt?: T;
createdAt?: T;
}
@ -4584,6 +5433,49 @@ export interface WorkflowsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tags_select".
*/
export interface TagsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
slug?: T;
description?: T;
color?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "authors_select".
*/
export interface AuthorsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
slug?: T;
avatar?: T;
bio?: T;
bioShort?: T;
title?: T;
email?: T;
website?: T;
social?:
| T
| {
twitter?: T;
linkedin?: T;
github?: T;
instagram?: T;
};
linkedTeam?: T;
linkedUser?: T;
isActive?: T;
isGuest?: T;
featured?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "cookie-configurations_select".

View file

@ -44,6 +44,10 @@ import { Timelines } from './collections/Timelines'
// Workflow Collection
import { Workflows } from './collections/Workflows'
// Blogging Collections
import { Tags } from './collections/Tags'
import { Authors } from './collections/Authors'
// Consent Management Collections
import { CookieConfigurations } from './collections/CookieConfigurations'
import { CookieInventory } from './collections/CookieInventory'
@ -161,6 +165,9 @@ export default buildConfig({
// Timelines & Workflows
Timelines,
Workflows,
// Blogging
Tags,
Authors,
// Consent Management
CookieConfigurations,
CookieInventory,
@ -212,6 +219,9 @@ export default buildConfig({
// Timeline & Workflow Collections
timelines: {},
workflows: {},
// Blogging Collections
tags: {},
authors: {},
// Consent Management Collections - customTenantField: true weil sie bereits ein tenant-Feld haben
'cookie-configurations': { customTenantField: true },
'cookie-inventory': { customTenantField: true },