feat: manage impressum page via payload

This commit is contained in:
syntaxbullet
2026-06-22 10:45:54 +02:00
parent 2e9ad5cdf8
commit e1713f6d6e
7 changed files with 625 additions and 184 deletions

View File

@@ -19,6 +19,7 @@
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts",
"preload:home-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-home-page-cms.ts",
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",

View File

@@ -0,0 +1,81 @@
import type { Field } from 'payload'
import { impressumContent } from '@/spa/impressumContent'
const text = (name: string, label: string, defaultValue?: string): Field => ({
name,
type: 'text',
label,
defaultValue,
})
const textarea = (name: string, label: string, defaultValue?: string): Field => ({
name,
type: 'textarea',
label,
defaultValue,
})
const sectionAdmin = (description: string) => ({
description,
initCollapsed: true,
})
export const impressumFields: Field[] = [
{
name: 'impressum',
label: 'Impressum page content',
type: 'group',
admin: {
description:
'Edit the Impressum page content. Legal section body blocks are stored as JSON so nested paragraphs, lists, notices, addresses, and subsections can keep their frontend order.',
},
fields: [
{
name: 'hero',
label: '01 · Hero',
type: 'group',
admin: sectionAdmin('Top legal page heading and description line.'),
fields: [
text('backLabel', 'Back link label', impressumContent.hero.backLabel),
text('eyebrow', 'Eyebrow', impressumContent.hero.eyebrow),
textarea('heading', 'Heading', impressumContent.hero.heading),
textarea('descriptionHtml', 'Description HTML', impressumContent.hero.descriptionHtml),
],
},
{
name: 'navigation',
label: '02 · Side navigation',
type: 'group',
admin: sectionAdmin('Sticky table-of-contents title and related legal-page link.'),
fields: [
text('tocTitle', 'TOC title', impressumContent.navigation.tocTitle),
text('sideLinkLabel', 'Side link label', impressumContent.navigation.sideLinkLabel),
text('sideLinkUrl', 'Side link URL', impressumContent.navigation.sideLinkUrl),
],
},
{
name: 'sections',
label: '03 · Legal sections',
type: 'array',
dbName: 'imp_sections',
admin: sectionAdmin('Each section controls one heading, table-of-contents label, and ordered JSON body blocks.'),
defaultValue: impressumContent.sections,
fields: [
text('id', 'Anchor ID'),
text('tocLabel', 'TOC label'),
text('title', 'Section title'),
{
name: 'items',
label: 'Content blocks JSON',
type: 'json',
admin: {
description:
'Array of blocks: paragraph, legal, address, list, or subsection. Paragraph/legal blocks use HTML strings for inline links.',
},
},
],
},
],
},
]

View File

@@ -10,6 +10,7 @@ import { MediaBlock } from '../../blocks/MediaBlock/config'
import { hero } from '@/heros/config'
import { contactFields } from './contactFields'
import { homeFields } from './homeFields'
import { impressumFields } from './impressumFields'
import { participationFields } from './participationFields'
import { slugField } from 'payload'
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
@@ -48,8 +49,19 @@ const isParticipationPage = (_: unknown, siblingData?: { slug?: string; spaPath?
return spaPath === '/teilnahme' || slug === 'teilnahme' || slug === 'participation' || title === 'teilnahme' || title === 'participation'
}
const isImpressumPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
const slug = siblingData?.slug
const spaPath = siblingData?.spaPath
const title = siblingData?.title?.toLowerCase()
return spaPath === '/impressum' || slug === 'impressum' || title === 'impressum'
}
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
isHomePage(undefined, siblingData) || isContactPage(undefined, siblingData) || isParticipationPage(undefined, siblingData)
isHomePage(undefined, siblingData) ||
isContactPage(undefined, siblingData) ||
isParticipationPage(undefined, siblingData) ||
isImpressumPage(undefined, siblingData)
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
@@ -138,6 +150,13 @@ export const Pages: CollectionConfig<'pages'> = {
fields: participationFields,
label: 'Participation Page',
},
{
admin: {
condition: (data) => isImpressumPage(undefined, data),
},
fields: impressumFields,
label: 'Impressum Page',
},
{
name: 'meta',
label: 'SEO',

View File

@@ -913,6 +913,50 @@ export interface Page {
};
};
};
/**
* Edit the Impressum page content. Legal section body blocks are stored as JSON so nested paragraphs, lists, notices, addresses, and subsections can keep their frontend order.
*/
impressum?: {
/**
* Top legal page heading and description line.
*/
hero?: {
backLabel?: string | null;
eyebrow?: string | null;
heading?: string | null;
descriptionHtml?: string | null;
};
/**
* Sticky table-of-contents title and related legal-page link.
*/
navigation?: {
tocTitle?: string | null;
sideLinkLabel?: string | null;
sideLinkUrl?: string | null;
};
/**
* Each section controls one heading, table-of-contents label, and ordered JSON body blocks.
*/
sections?:
| {
id?: string | null;
tocLabel?: string | null;
title?: string | null;
/**
* Array of blocks: paragraph, legal, address, list, or subsection. Paragraph/legal blocks use HTML strings for inline links.
*/
items?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
}[]
| null;
};
meta?: {
title?: string | null;
/**
@@ -2514,6 +2558,33 @@ export interface PagesSelect<T extends boolean = true> {
};
};
};
impressum?:
| T
| {
hero?:
| T
| {
backLabel?: T;
eyebrow?: T;
heading?: T;
descriptionHtml?: T;
};
navigation?:
| T
| {
tocTitle?: T;
sideLinkLabel?: T;
sideLinkUrl?: T;
};
sections?:
| T
| {
id?: T;
tocLabel?: T;
title?: T;
items?: T;
};
};
meta?:
| T
| {

View File

@@ -0,0 +1,41 @@
import 'dotenv/config'
import config from '@payload-config'
import { getPayload } from 'payload'
import { impressumContent } from '@/spa/impressumContent'
async function main() {
const payload = await getPayload({ config })
const pageResult = await payload.find({
collection: 'pages',
limit: 1,
pagination: false,
draft: true,
overrideAccess: true,
where: {
or: [{ spaPath: { equals: '/impressum' } }, { slug: { equals: 'impressum' } }],
},
})
const page = pageResult.docs[0]
if (!page) throw new Error('Impressum page not found. Expected spaPath=/impressum or slug=impressum.')
await payload.update({
collection: 'pages',
id: page.id,
overrideAccess: true,
context: { disableRevalidate: true },
data: {
impressum: impressumContent,
} as never,
})
payload.logger.info(`Preloaded Impressum CMS fields for page ${page.id}`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})

295
src/spa/impressumContent.ts Normal file
View File

@@ -0,0 +1,295 @@
export type LegalContentItem =
| { type: 'paragraph'; html: string; variant?: 'body' | 'strong' | 'italic' }
| { type: 'address'; lines: string[] }
| { type: 'legal'; html: string }
| { type: 'list'; items: string[] }
| { type: 'subsection'; num: string; title: string; items: LegalContentItem[] }
export type LegalSectionContent = {
id: string
tocLabel: string
title: string
items: LegalContentItem[]
}
export const vmAddressLines = [
'VM Verbands-Management GmbH',
'Edelsbergstr. 8',
'80686 München',
'Tel. +49 (0) 89 57007-0',
'Fax +49 (0) 89 57007 260',
'info@vm-verbands-management.de',
'HRB 110222 AG München, gesetzl. vertreten durch Friedrich Marx',
]
const navyLink = 'style="color:#111D55;font-weight:600"'
const navyPlainLink = 'style="color:#111D55"'
export const impressumContent = {
hero: {
backLabel: 'Zurück zur Website',
eyebrow: 'Legal',
heading: 'Impressum',
descriptionHtml:
'Angaben gemäß §5 TMG · <a href="/datenschutz" style="color:rgba(239,191,4,0.7);text-decoration:none">Zur Datenschutzerklärung →</a>',
},
navigation: {
tocTitle: 'Inhalt',
sideLinkLabel: 'Datenschutzerklärung →',
sideLinkUrl: '/datenschutz',
},
sections: [
{
id: 'betreiber',
tocLabel: 'Angaben gem. §5 TMG',
title: 'Angaben gemäß §5 TMG',
items: [
{ type: 'paragraph', variant: 'strong', html: 'Informationen über den Verantwortlichen' },
{ type: 'address', lines: vmAddressLines },
{ type: 'paragraph', variant: 'strong', html: 'Angaben zum Webseitenbetreiber' },
{ type: 'address', lines: vmAddressLines },
{
type: 'paragraph',
html: 'Inhaltlich Verantwortlicher gemäß TMG: VM Verbands-Management GmbH, gesetzl. vertreten durch Friedrich Marx',
},
],
},
{
id: 'datenschutz',
tocLabel: 'Datenschutz',
title: 'Datenschutz & Datenverarbeitung',
items: [
{
type: 'paragraph',
html: `Alle personenbezogenen Informationen werden vertraulich und gemäß den gesetzlichen Vorschriften behandelt, wie in der <a href="/datenschutz" ${navyLink}>Datenschutzerklärung</a> erläutert. Den Schutz der persönlichen Daten nehmen wir als Webseitenbetreiber sehr ernst.`,
},
{
type: 'paragraph',
html: 'Ein Besuch unserer Webseiten ist möglich, ohne Angaben zu Ihrer Person zu machen. Es werden bei jedem Zugriff auf unsere Website Nutzungsdaten durch Ihren Internetbrowser übermittelt und in Protokolldaten (Server-Logfiles) gespeichert. Zu diesen gespeicherten Daten gehören z. B. Name der aufgerufenen Seite, Datum und Uhrzeit des Abrufs, übertragene Datenmenge und der anfragende Provider (IP-Adresse). Diese Daten dienen ausschließlich der Gewährleistung eines störungsfreien Betriebs unserer Website und zur Verbesserung unseres Angebotes. Eine Zuordnung dieser Daten zu einer bestimmten Person ist nicht möglich.',
},
{ type: 'paragraph', html: 'Es erfolgt keine Zusammenführung dieser Daten mit anderen Datenquellen.' },
],
},
{
id: 'kontaktformular',
tocLabel: 'Kontaktformular',
title: 'Erhebung und Verarbeitung beim Kontaktformular',
items: [
{
type: 'paragraph',
html: 'Sofern Sie uns über unsere Kontaktdaten eine E-Mail übersenden bzw. uns über das Kontaktformular informieren, erheben wir Ihre personenbezogenen Daten (Name, E-Mail-Adresse, Telefonnummer, Betreff, Nachrichtentext) nur in dem von Ihnen zur Verfügung gestellten Umfang. Die Datenverarbeitung dient dem Zweck der Kontaktaufnahme.',
},
{ type: 'paragraph', html: 'Mit Absenden Ihrer Nachricht willigen Sie in die Verarbeitung der übermittelten Daten ein.' },
{ type: 'legal', html: 'Eine Verarbeitung erfolgt auf Grundlage des Art. 6 (1) lit. a DSGVO mit Ihrer Einwilligung.' },
{
type: 'paragraph',
html: 'Sie können diese Einwilligung jederzeit durch Mitteilung an uns widerrufen. Ihre E-Mail-Adresse wird nur zur Bearbeitung Ihrer Anfrage genutzt. Ihre Daten werden anschließend gelöscht, sofern Sie der weitergehenden Verarbeitung und Nutzung nicht zugestimmt haben.',
},
],
},
{
id: 'betroffenenrechte',
tocLabel: 'Betroffenenrechte',
title: 'Betroffenenrechte',
items: [
{
type: 'paragraph',
html: 'Bei Vorliegen der gesetzlichen Voraussetzungen stehen Ihnen folgende Rechte nach Art. 15 bis 20 DSGVO zu:',
},
{
type: 'list',
items: [
'Recht auf Auskunft',
'Recht auf Berichtigung',
'Recht auf Löschung',
'Recht auf Einschränkung der Verarbeitung',
'Recht auf Datenübertragbarkeit',
],
},
{
type: 'legal',
html: 'Außerdem steht Ihnen nach Art. 21 (1) DSGVO ein Widerspruchsrecht gegen die Verarbeitungen zu, die auf Art. 6 (1) f DSGVO beruhen, sowie gegen die Verarbeitung zum Zwecke von Direktwerbung.',
},
{
type: 'paragraph',
html: `Auf Wunsch können Sie uns kontaktieren. Die Kontaktdaten finden Sie unter <a href="/kontakt" ${navyLink}>Kontakt</a>.`,
},
],
},
{
id: 'aufsichtsbehoerde',
tocLabel: 'Beschwerderecht',
title: 'Beschwerderecht bei der Aufsichtsbehörde',
items: [
{
type: 'paragraph',
html: 'Sie haben gemäß Art. 77 DSGVO das Recht, sich bei der Aufsichtsbehörde zu beschweren, wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer personenbezogenen Daten nicht rechtmäßig erfolgt.',
},
{ type: 'paragraph', variant: 'strong', html: 'Die zuständige Aufsichtsbehörde:' },
{
type: 'address',
lines: [
'Bayerisches Landesamt für Datenschutzaufsicht',
'Postfach 606',
'91511 Ansbach',
'Deutschland',
'Tel. 09853-1300',
'Fax 09853-981300',
'E-Mail: poststelle@lda.bayern.de',
],
},
],
},
{
id: 'verantwortlicher',
tocLabel: 'Verantwortlicher',
title: 'Name und Anschrift des Verantwortlichen',
items: [
{
type: 'paragraph',
html: 'Verantwortlicher im Sinne der Datenschutz-Grundverordnung und sonstiger datenschutzrechtlicher Bestimmungen ist der Webseitenbetreiber:',
},
{ type: 'address', lines: vmAddressLines },
],
},
{
id: 'privacy-policy',
tocLabel: 'Privacy Policy',
title: 'Erklärung zum Datenschutz (Privacy Policy)',
items: [
{
type: 'paragraph',
variant: 'italic',
html: 'Die Betreiber des Portals nehmen den Schutz der privaten Daten ernst. Die besondere Beachtung der Privatsphäre bei der Verarbeitung persönlicher Daten ist ein wichtiges Anliegen. Persönliche Daten werden gemäß den Bestimmungen des Bundesdatenschutzgesetzes BDSG verwendet; die Betreiber dieser Website verpflichten sich zur Verschwiegenheit. Diese Webseiten können Links zu Webseiten anderer Anbieter enthalten, auf die sich diese Datenschutzerklärung nicht erstreckt. Weitere wichtige Informationen finden sich auch in den Allgemeinen Nutzungsbedingungen.',
},
{
type: 'subsection',
num: '1',
title: 'Personenbezogene Daten',
items: [
{
type: 'paragraph',
html: 'Personenbezogene Daten sind Informationen, die dazu genutzt werden können, die Identität zu erfahren. Darunter fallen Informationen wie richtiger Name, Adresse, Postanschrift, Telefonnummer. Informationen, die nicht direkt mit der wirklichen Identität in Verbindung gebracht werden (wie zum Beispiel favorisierte Webseiten oder Anzahl der Nutzer einer Site) fallen nicht darunter.',
},
{
type: 'paragraph',
html: 'Die statistische Auswertung anonymisierter Datensätze bleibt vorbehalten. Wir nutzen die persönlichen Daten zu Zwecken der technischen Administration der Webseiten und zur Kundenverwaltung nur im jeweils dafür erforderlichen Umfang. Darüber hinaus werden persönliche Daten nur dann gespeichert, wenn diese freiwillig angegeben werden.',
},
],
},
{
type: 'subsection',
num: '2',
title: 'Weitergabe personenbezogener Daten',
items: [
{
type: 'paragraph',
html: 'Wir verwenden personenbezogene Informationen nur für diese Webseite. Wir geben die Informationen nicht ohne ausdrückliches Einverständnis an Dritte weiter. Sollten im Rahmen der Auftragsdatenverarbeitung Daten an Dienstleister weitergegeben werden, so sind diese an das Bundesdatenschutzgesetz BDSG, andere gesetzliche Vorschriften und an diese Privacy Policy gebunden.',
},
{
type: 'paragraph',
html: 'Erhebungen beziehungsweise Übermittlungen persönlicher Daten an staatliche Einrichtungen und Behörden erfolgen nur im Rahmen zwingender Rechtsvorschriften.',
},
{
type: 'paragraph',
html: `Bitte beachten Sie gesondert die Hinweise zur Datenverarbeitung im Zusammenhang mit Google reCAPTCHA unter <a href="/datenschutz#recaptcha" ${navyPlainLink}>Datenschutz</a>.`,
},
],
},
{
type: 'subsection',
num: '3',
title: 'Kinder',
items: [
{
type: 'paragraph',
html: 'Personen unter 18 Jahren sollten ohne Zustimmung der Eltern oder Erziehungsberechtigten keine personenbezogenen Daten an uns übermitteln. Wir fordern keine personenbezogenen Daten von Kindern an, sammeln diese nicht und geben sie nicht an Dritte weiter.',
},
],
},
{
type: 'subsection',
num: '4',
title: 'Recht auf Widerruf',
items: [
{
type: 'paragraph',
html: 'Wenn Sie uns personenbezogene Daten überlassen haben, können Sie diese jederzeit im Benutzerprofil wieder ändern und löschen. Für eine vollständige Löschung des Accounts bitte an den Webmaster wenden. Bis zu diesem Zeitpunkt erfolgte Beiträge in Foren, Kommentaren, Terminankündigungen und Artikeln bleiben allerdings unter Umständen erhalten Informationen dazu auch bei den allgemeinen Nutzungsbedingungen.',
},
],
},
{
type: 'subsection',
num: '5',
title: 'Links zu anderen Websites',
items: [
{
type: 'paragraph',
html: 'Unser Online-Angebot enthält Links zu anderen Websites. Wir haben keinen Einfluss darauf, dass deren Betreiber die Datenschutzbestimmungen einhalten.',
},
],
},
{
type: 'subsection',
num: '6',
title: 'Beiträge',
items: [
{
type: 'paragraph',
html: 'Die Beiträge auf unserer Seite sind für jeden zugänglich. Beiträge sollten vor der Veröffentlichung sorgfältig darauf überprüft werden, ob sie Angaben enthalten, die nicht für die Öffentlichkeit bestimmt sind. Die Beiträge werden möglicherweise in Suchmaschinen erfasst und auch ohne gezielten Aufruf dieser Website weltweit zugreifbar.',
},
{
type: 'paragraph',
html: 'Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich. Alle hier verwendeten Namen, Begriffe, Zeichen und Grafiken können Marken- oder Warenzeichen im Besitze ihrer rechtlichen Eigentümer sein. Die Rechte aller erwähnten und benutzten Marken- und Warenzeichen liegen ausschließlich bei deren Besitzern.',
},
],
},
{
type: 'subsection',
num: '7',
title: 'Fragen und Kommentare',
items: [
{ type: 'paragraph', html: 'Bei Fragen und für Anregungen und Kommentare zum Thema Datenschutz bitte per Mail an den Webmaster wenden.' },
{ type: 'address', lines: vmAddressLines },
],
},
],
},
{
id: 'bildnachweise',
tocLabel: 'Bildnachweise',
title: 'Bildnachweise',
items: [
{
type: 'list',
items: [
'Headline Maximilianeum: © Dietmar Egerer, München',
'Ingrid Großmann, Taufkirchen',
'EWIF Deutschland, München',
'icon.icons.com',
'Bilder zum Bayerischen Mittelstandspreis: © VM Verbands-Management GmbH, gesetzl. vertreten durch Friedrich Marx',
],
},
],
},
{
id: 'haftung',
tocLabel: 'Haftungsausschluss',
title: 'Haftungsausschluss',
items: [
{
type: 'paragraph',
html: 'Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich.',
},
{
type: 'paragraph',
html: 'Alle hier verwendeten Namen, Begriffe, Zeichen und Grafiken können Marken- oder Warenzeichen im Besitze ihrer rechtlichen Eigentümer sein. Die Rechte aller erwähnten und benutzten Marken- und Warenzeichen liegen ausschließlich bei deren Besitzern.',
},
{
type: 'paragraph',
html: `Einen allgemeinen Haftungsausschluss finden Sie unter: <a href="https://www.disclaimer.de/disclaimer.htm" target="_blank" rel="noopener noreferrer" ${navyPlainLink}>disclaimer.de</a>`,
},
],
},
] satisfies LegalSectionContent[],
}

View File

@@ -1,215 +1,105 @@
import React from 'react';
import { Link } from '@/spa/router';
import { ArrowLeft } from 'lucide-react';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import React from 'react'
import { ArrowLeft } from 'lucide-react'
const ANTHRA = '#1A2030';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
const CREAM = '#EFE5E3';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
import { useCmsRoute } from '@/spa/cmsRoute'
import { impressumContent, type LegalContentItem, type LegalSectionContent } from '@/spa/impressumContent'
import { useIsMobile } from '@/spa/hooks/useIsMobile'
import { Link } from '@/spa/router'
const TOC = [
{ id: 'betreiber', label: 'Angaben gem. §5 TMG' },
{ id: 'datenschutz', label: 'Datenschutz' },
{ id: 'kontaktformular', label: 'Kontaktformular' },
{ id: 'betroffenenrechte', label: 'Betroffenenrechte' },
{ id: 'aufsichtsbehoerde', label: 'Beschwerderecht' },
{ id: 'verantwortlicher', label: 'Verantwortlicher' },
{ id: 'privacy-policy', label: 'Privacy Policy' },
{ id: 'bildnachweise', label: 'Bildnachweise' },
{ id: 'haftung', label: 'Haftungsausschluss' },
];
const ANTHRA = '#1A2030'
const NAVY = '#111D55'
const GOLD = '#EFBF04'
const CREAM = '#EFE5E3'
const FF = '"IBM Plex Sans", sans-serif'
const FB = '"Inter", sans-serif'
const ADDR = (
<>
VM Verbands-Management GmbH<br />
Edelsbergstr. 8<br />
80686 München<br />
Tel. +49 (0) 89 57007-0<br />
Fax +49 (0) 89 57007 260<br />
info@vm-verbands-management.de<br />
HRB 110222 AG München, gesetzl. vertreten durch Friedrich Marx
</>
);
type ImpressumCms = Partial<typeof impressumContent>
const fallbackText = (value: unknown, fallback: string) => (typeof value === 'string' && value.length > 0 ? value : fallback)
const fallbackArray = <T,>(value: unknown, fallback: T[]) => (Array.isArray(value) ? (value as T[]) : fallback)
const isLegalItem = (item: unknown): item is LegalContentItem =>
Boolean(item && typeof item === 'object' && 'type' in item && typeof (item as { type?: unknown }).type === 'string')
export default function Impressum() {
const isMobile = useIsMobile();
const isMobile = useIsMobile()
const cms = (useCmsRoute()?.doc?.impressum || {}) as ImpressumCms
const hero = { ...impressumContent.hero, ...(cms.hero || {}) }
const navigation = { ...impressumContent.navigation, ...(cms.navigation || {}) }
const sections = fallbackArray<LegalSectionContent>(cms.sections, impressumContent.sections)
return (
<div style={{ background: CREAM, minHeight: '100vh' }}>
{/* ── HERO ──────────────────────────────────────────────────────────── */}
<section style={{ background: ANTHRA, padding: isMobile ? '48px 24px 40px' : '64px 80px 52px', position: 'relative', overflow: 'hidden' }}>
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.2), transparent)` }} />
<Link
to="/"
style={{ display: 'inline-flex', alignItems: 'center', gap: 7, fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: 'rgba(255,255,255,0.4)', textDecoration: 'none', marginBottom: 32, transition: 'color 0.15s' }}
onMouseEnter={e => (e.currentTarget.style.color = '#fff')}
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.4)')}
onMouseEnter={(e) => (e.currentTarget.style.color = '#fff')}
onMouseLeave={(e) => (e.currentTarget.style.color = 'rgba(255,255,255,0.4)')}
>
<ArrowLeft size={12} /> Zurück zur Website
<ArrowLeft size={12} /> {fallbackText(hero.backLabel, impressumContent.hero.backLabel)}
</Link>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<div style={{ width: 32, height: 2, background: GOLD }} />
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.35em', textTransform: 'uppercase', color: GOLD }}>Legal</span>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.35em', textTransform: 'uppercase', color: GOLD }}>{fallbackText(hero.eyebrow, impressumContent.hero.eyebrow)}</span>
</div>
<h1 style={{ fontFamily: FF, fontWeight: 900, fontSize: isMobile ? 'clamp(1.8rem, 7vw, 2.6rem)' : 'clamp(2.4rem, 4vw, 3.2rem)', color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.02, margin: '0 0 12px' }}>
Impressum
{fallbackText(hero.heading, impressumContent.hero.heading)}
</h1>
<p style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.45)', margin: 0 }}>
Angaben gemäß §5 TMG · <Link to="/datenschutz" style={{ color: 'rgba(239,191,4,0.7)', textDecoration: 'none' }}>Zur Datenschutzerklärung </Link>
</p>
<p
style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.45)', margin: 0 }}
dangerouslySetInnerHTML={{ __html: fallbackText(hero.descriptionHtml, impressumContent.hero.descriptionHtml) }}
/>
</section>
{/* ── BODY ──────────────────────────────────────────────────────────── */}
<div style={{ maxWidth: 1100, margin: '0 auto', padding: isMobile ? '40px 24px' : '64px 80px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '200px 1fr', gap: isMobile ? 40 : 80, alignItems: 'start' }}>
{/* LEFT sticky TOC */}
{!isMobile && (
<nav style={{ position: 'sticky', top: 80 }}>
<div style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: GOLD, marginBottom: 16 }}>Inhalt</div>
<div style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: GOLD, marginBottom: 16 }}>{fallbackText(navigation.tocTitle, impressumContent.navigation.tocTitle)}</div>
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>
{TOC.map(item => (
{sections.map((item) => (
<li key={item.id}>
<a
href={`#${item.id}`}
style={{ display: 'block', fontFamily: FB, fontSize: 12, color: 'rgba(17,29,85,0.5)', textDecoration: 'none', padding: '5px 0 5px 12px', borderLeft: '2px solid transparent', transition: 'color 0.15s, border-color 0.15s', lineHeight: 1.4 }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = NAVY; (e.currentTarget as HTMLElement).style.borderLeftColor = GOLD; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'rgba(17,29,85,0.5)'; (e.currentTarget as HTMLElement).style.borderLeftColor = 'transparent'; }}
>{item.label}</a>
onMouseEnter={(e) => {
e.currentTarget.style.color = NAVY
e.currentTarget.style.borderLeftColor = GOLD
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = 'rgba(17,29,85,0.5)'
e.currentTarget.style.borderLeftColor = 'transparent'
}}
>
{fallbackText(item.tocLabel, item.title)}
</a>
</li>
))}
</ul>
<div style={{ marginTop: 32, paddingTop: 20, borderTop: '1px solid rgba(17,29,85,0.1)' }}>
<Link to="/datenschutz" style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.14em', color: NAVY, textDecoration: 'none' }}>
Datenschutzerklärung
<Link to={fallbackText(navigation.sideLinkUrl, impressumContent.navigation.sideLinkUrl)} style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.14em', color: NAVY, textDecoration: 'none' }}>
{fallbackText(navigation.sideLinkLabel, impressumContent.navigation.sideLinkLabel)}
</Link>
</div>
</nav>
)}
{/* RIGHT content */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 56 }}>
<Section id="betreiber" title="Angaben gemäß §5 TMG">
<p style={{ ...body, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Informationen über den Verantwortlichen</p>
<AddressBlock>{ADDR}</AddressBlock>
<p style={{ ...body, fontWeight: 600, color: NAVY, marginBottom: 8 }}>Angaben zum Webseitenbetreiber</p>
<AddressBlock>{ADDR}</AddressBlock>
<p style={body}>Inhaltlich Verantwortlicher gemäß TMG: VM Verbands-Management GmbH, gesetzl. vertreten durch Friedrich Marx</p>
</Section>
<Section id="datenschutz" title="Datenschutz & Datenverarbeitung">
<p style={body}>Alle personenbezogenen Informationen werden vertraulich und gemäß den gesetzlichen Vorschriften behandelt, wie in der <Link to="/datenschutz" style={{ color: NAVY, fontWeight: 600 }}>Datenschutzerklärung</Link> erläutert. Den Schutz der persönlichen Daten nehmen wir als Webseitenbetreiber sehr ernst.</p>
<p style={body}>Ein Besuch unserer Webseiten ist möglich, ohne Angaben zu Ihrer Person zu machen. Es werden bei jedem Zugriff auf unsere Website Nutzungsdaten durch Ihren Internetbrowser übermittelt und in Protokolldaten (Server-Logfiles) gespeichert. Zu diesen gespeicherten Daten gehören z. B. Name der aufgerufenen Seite, Datum und Uhrzeit des Abrufs, übertragene Datenmenge und der anfragende Provider (IP-Adresse). Diese Daten dienen ausschließlich der Gewährleistung eines störungsfreien Betriebs unserer Website und zur Verbesserung unseres Angebotes. Eine Zuordnung dieser Daten zu einer bestimmten Person ist nicht möglich.</p>
<p style={body}>Es erfolgt keine Zusammenführung dieser Daten mit anderen Datenquellen.</p>
</Section>
<Section id="kontaktformular" title="Erhebung und Verarbeitung beim Kontaktformular">
<p style={body}>Sofern Sie uns über unsere Kontaktdaten eine E-Mail übersenden bzw. uns über das Kontaktformular informieren, erheben wir Ihre personenbezogenen Daten (Name, E-Mail-Adresse, Telefonnummer, Betreff, Nachrichtentext) nur in dem von Ihnen zur Verfügung gestellten Umfang. Die Datenverarbeitung dient dem Zweck der Kontaktaufnahme.</p>
<p style={body}>Mit Absenden Ihrer Nachricht willigen Sie in die Verarbeitung der übermittelten Daten ein.</p>
<Legal>Eine Verarbeitung erfolgt auf Grundlage des Art. 6 (1) lit. a DSGVO mit Ihrer Einwilligung.</Legal>
<p style={body}>Sie können diese Einwilligung jederzeit durch Mitteilung an uns widerrufen. Ihre E-Mail-Adresse wird nur zur Bearbeitung Ihrer Anfrage genutzt. Ihre Daten werden anschließend gelöscht, sofern Sie der weitergehenden Verarbeitung und Nutzung nicht zugestimmt haben.</p>
</Section>
<Section id="betroffenenrechte" title="Betroffenenrechte">
<p style={body}>Bei Vorliegen der gesetzlichen Voraussetzungen stehen Ihnen folgende Rechte nach Art. 15 bis 20 DSGVO zu:</p>
<ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 6 }}>
<li>Recht auf Auskunft</li>
<li>Recht auf Berichtigung</li>
<li>Recht auf Löschung</li>
<li>Recht auf Einschränkung der Verarbeitung</li>
<li>Recht auf Datenübertragbarkeit</li>
</ul>
<Legal>Außerdem steht Ihnen nach Art. 21 (1) DSGVO ein Widerspruchsrecht gegen die Verarbeitungen zu, die auf Art. 6 (1) f DSGVO beruhen, sowie gegen die Verarbeitung zum Zwecke von Direktwerbung.</Legal>
<p style={body}>Auf Wunsch können Sie uns kontaktieren. Die Kontaktdaten finden Sie unter <Link to="/kontakt" style={{ color: NAVY, fontWeight: 600 }}>Kontakt</Link>.</p>
</Section>
<Section id="aufsichtsbehoerde" title="Beschwerderecht bei der Aufsichtsbehörde">
<p style={body}>Sie haben gemäß Art. 77 DSGVO das Recht, sich bei der Aufsichtsbehörde zu beschweren, wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer personenbezogenen Daten nicht rechtmäßig erfolgt.</p>
<p style={{ ...body, fontWeight: 600, color: NAVY }}>Die zuständige Aufsichtsbehörde:</p>
<AddressBlock>
Bayerisches Landesamt für Datenschutzaufsicht<br />
Postfach 606<br />
91511 Ansbach<br />
Deutschland<br />
Tel. 09853-1300<br />
Fax 09853-981300<br />
E-Mail: poststelle@lda.bayern.de
</AddressBlock>
</Section>
<Section id="verantwortlicher" title="Name und Anschrift des Verantwortlichen">
<p style={body}>Verantwortlicher im Sinne der Datenschutz-Grundverordnung und sonstiger datenschutzrechtlicher Bestimmungen ist der Webseitenbetreiber:</p>
<AddressBlock>{ADDR}</AddressBlock>
</Section>
<Section id="privacy-policy" title="Erklärung zum Datenschutz (Privacy Policy)">
<p style={{ ...body, fontStyle: 'italic', borderLeft: `3px solid ${GOLD}`, paddingLeft: 16, marginBottom: 32 }}>
Die Betreiber des Portals nehmen den Schutz der privaten Daten ernst. Die besondere Beachtung der Privatsphäre bei der Verarbeitung persönlicher Daten ist ein wichtiges Anliegen. Persönliche Daten werden gemäß den Bestimmungen des Bundesdatenschutzgesetzes BDSG verwendet; die Betreiber dieser Website verpflichten sich zur Verschwiegenheit. Diese Webseiten können Links zu Webseiten anderer Anbieter enthalten, auf die sich diese Datenschutzerklärung nicht erstreckt. Weitere wichtige Informationen finden sich auch in den Allgemeinen Nutzungsbedingungen.
</p>
<SubSection num="1" title="Personenbezogene Daten">
<p style={body}>Personenbezogene Daten sind Informationen, die dazu genutzt werden können, die Identität zu erfahren. Darunter fallen Informationen wie richtiger Name, Adresse, Postanschrift, Telefonnummer. Informationen, die nicht direkt mit der wirklichen Identität in Verbindung gebracht werden (wie zum Beispiel favorisierte Webseiten oder Anzahl der Nutzer einer Site) fallen nicht darunter.</p>
<p style={body}>Die statistische Auswertung anonymisierter Datensätze bleibt vorbehalten. Wir nutzen die persönlichen Daten zu Zwecken der technischen Administration der Webseiten und zur Kundenverwaltung nur im jeweils dafür erforderlichen Umfang. Darüber hinaus werden persönliche Daten nur dann gespeichert, wenn diese freiwillig angegeben werden.</p>
</SubSection>
<SubSection num="2" title="Weitergabe personenbezogener Daten">
<p style={body}>Wir verwenden personenbezogene Informationen nur für diese Webseite. Wir geben die Informationen nicht ohne ausdrückliches Einverständnis an Dritte weiter. Sollten im Rahmen der Auftragsdatenverarbeitung Daten an Dienstleister weitergegeben werden, so sind diese an das Bundesdatenschutzgesetz BDSG, andere gesetzliche Vorschriften und an diese Privacy Policy gebunden.</p>
<p style={body}>Erhebungen beziehungsweise Übermittlungen persönlicher Daten an staatliche Einrichtungen und Behörden erfolgen nur im Rahmen zwingender Rechtsvorschriften.</p>
<p style={body}>Bitte beachten Sie gesondert die Hinweise zur Datenverarbeitung im Zusammenhang mit Google reCAPTCHA unter <Link to="/datenschutz#recaptcha" style={{ color: NAVY }}>Datenschutz</Link>.</p>
</SubSection>
<SubSection num="3" title="Kinder">
<p style={body}>Personen unter 18 Jahren sollten ohne Zustimmung der Eltern oder Erziehungsberechtigten keine personenbezogenen Daten an uns übermitteln. Wir fordern keine personenbezogenen Daten von Kindern an, sammeln diese nicht und geben sie nicht an Dritte weiter.</p>
</SubSection>
<SubSection num="4" title="Recht auf Widerruf">
<p style={body}>Wenn Sie uns personenbezogene Daten überlassen haben, können Sie diese jederzeit im Benutzerprofil wieder ändern und löschen. Für eine vollständige Löschung des Accounts bitte an den Webmaster wenden. Bis zu diesem Zeitpunkt erfolgte Beiträge in Foren, Kommentaren, Terminankündigungen und Artikeln bleiben allerdings unter Umständen erhalten Informationen dazu auch bei den allgemeinen Nutzungsbedingungen.</p>
</SubSection>
<SubSection num="5" title="Links zu anderen Websites">
<p style={body}>Unser Online-Angebot enthält Links zu anderen Websites. Wir haben keinen Einfluss darauf, dass deren Betreiber die Datenschutzbestimmungen einhalten.</p>
</SubSection>
<SubSection num="6" title="Beiträge">
<p style={body}>Die Beiträge auf unserer Seite sind für jeden zugänglich. Beiträge sollten vor der Veröffentlichung sorgfältig darauf überprüft werden, ob sie Angaben enthalten, die nicht für die Öffentlichkeit bestimmt sind. Die Beiträge werden möglicherweise in Suchmaschinen erfasst und auch ohne gezielten Aufruf dieser Website weltweit zugreifbar.</p>
<p style={body}>Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich. Alle hier verwendeten Namen, Begriffe, Zeichen und Grafiken können Marken- oder Warenzeichen im Besitze ihrer rechtlichen Eigentümer sein. Die Rechte aller erwähnten und benutzten Marken- und Warenzeichen liegen ausschließlich bei deren Besitzern.</p>
</SubSection>
<SubSection num="7" title="Fragen und Kommentare">
<p style={body}>Bei Fragen und für Anregungen und Kommentare zum Thema Datenschutz bitte per Mail an den Webmaster wenden.</p>
<AddressBlock>{ADDR}</AddressBlock>
</SubSection>
</Section>
<Section id="bildnachweise" title="Bildnachweise">
<ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 6 }}>
<li>Headline Maximilianeum: © Dietmar Egerer, München</li>
<li>Ingrid Großmann, Taufkirchen</li>
<li>EWIF Deutschland, München</li>
<li>icon.icons.com</li>
<li>Bilder zum Bayerischen Mittelstandspreis: © VM Verbands-Management GmbH, gesetzl. vertreten durch Friedrich Marx</li>
</ul>
</Section>
<Section id="haftung" title="Haftungsausschluss">
<p style={body}>Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links. Für den Inhalt der verlinkten Seiten sind ausschließlich deren Betreiber verantwortlich.</p>
<p style={body}>Alle hier verwendeten Namen, Begriffe, Zeichen und Grafiken können Marken- oder Warenzeichen im Besitze ihrer rechtlichen Eigentümer sein. Die Rechte aller erwähnten und benutzten Marken- und Warenzeichen liegen ausschließlich bei deren Besitzern.</p>
<p style={body}>Einen allgemeinen Haftungsausschluss finden Sie unter:{' '}
<a href="https://www.disclaimer.de/disclaimer.htm" target="_blank" rel="noopener noreferrer" style={{ color: NAVY }}>disclaimer.de</a>
</p>
</Section>
{sections.map((section) => (
<Section key={section.id} id={section.id} title={section.title}>
{fallbackArray<LegalContentItem>(section.items, []).filter(isLegalItem).map((item, index) => (
<ContentItem key={`${item.type}-${index}`} item={item} />
))}
</Section>
))}
</div>
</div>
</div>
);
)
}
/* ── shared styles ── */
const body: React.CSSProperties = {
fontFamily: '"Inter", sans-serif',
fontSize: 15,
@@ -217,7 +107,7 @@ const body: React.CSSProperties = {
lineHeight: 1.85,
margin: '0 0 16px',
overflowWrap: 'anywhere',
};
}
function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
return (
@@ -227,11 +117,48 @@ function Section({ id, title, children }: { id: string; title: string; children:
{title}
</h2>
</div>
<div style={{ background: '#fff', padding: 'clamp(20px, 5vw, 28px) clamp(20px, 5vw, 32px)', boxShadow: '0 1px 3px rgba(0,0,0,0.06)' }}>
{children}
</div>
<div style={{ background: '#fff', padding: 'clamp(20px, 5vw, 28px) clamp(20px, 5vw, 32px)', boxShadow: '0 1px 3px rgba(0,0,0,0.06)' }}>{children}</div>
</div>
);
)
}
function ContentItem({ item }: { item: LegalContentItem }) {
switch (item.type) {
case 'address':
return <AddressBlock lines={item.lines} />
case 'legal':
return <Legal html={item.html} />
case 'list':
return (
<ul style={{ ...body, paddingLeft: 20, display: 'flex', flexDirection: 'column', gap: 6 }}>
{item.items.map((entry, index) => (
<li key={`${entry}-${index}`}>{entry}</li>
))}
</ul>
)
case 'subsection':
return (
<SubSection num={item.num} title={item.title}>
{fallbackArray<LegalContentItem>(item.items, []).filter(isLegalItem).map((subItem, index) => (
<ContentItem key={`${subItem.type}-${index}`} item={subItem} />
))}
</SubSection>
)
case 'paragraph':
default:
return <Paragraph item={item} />
}
}
function Paragraph({ item }: { item: Extract<LegalContentItem, { type: 'paragraph' }> }) {
const variantStyle: React.CSSProperties =
item.variant === 'strong'
? { fontWeight: 600, color: NAVY, marginBottom: 8 }
: item.variant === 'italic'
? { fontStyle: 'italic', borderLeft: `3px solid ${GOLD}`, paddingLeft: 16, marginBottom: 32 }
: {}
return <p style={{ ...body, ...variantStyle }} dangerouslySetInnerHTML={{ __html: item.html }} />
}
function SubSection({ num, title, children }: { num: string; title: string; children: React.ReactNode }) {
@@ -243,23 +170,29 @@ function SubSection({ num, title, children }: { num: string; title: string; chil
</div>
{children}
</div>
);
)
}
function AddressBlock({ children }: { children: React.ReactNode }) {
const isMobile = useIsMobile();
function AddressBlock({ lines }: { lines: string[] }) {
const isMobile = useIsMobile()
return (
<address style={{ fontFamily: '"Inter", sans-serif', fontSize: isMobile ? 15 : 14, color: 'rgba(58,58,58,0.75)', lineHeight: 1.9, fontStyle: 'normal', background: 'rgba(17,29,85,0.04)', padding: '16px 20px', borderLeft: '2px solid rgba(239,191,4,0.4)', marginBottom: 16, overflowWrap: 'anywhere' }}>
{children}
{lines.map((line, index) => (
<React.Fragment key={`${line}-${index}`}>
{line}
{index < lines.length - 1 && <br />}
</React.Fragment>
))}
</address>
);
)
}
function Legal({ children }: { children: React.ReactNode }) {
const isMobile = useIsMobile();
function Legal({ html }: { html: string }) {
const isMobile = useIsMobile()
return (
<p style={{ fontFamily: '"Inter", sans-serif', fontSize: isMobile ? 15 : 14, color: NAVY, lineHeight: 1.75, background: 'rgba(17,29,85,0.04)', padding: '12px 16px', borderLeft: `3px solid ${NAVY}`, margin: '0 0 16px', fontWeight: 500, overflowWrap: 'anywhere' }}>
{children}
</p>
);
<p
style={{ fontFamily: '"Inter", sans-serif', fontSize: isMobile ? 15 : 14, color: NAVY, lineHeight: 1.75, background: 'rgba(17,29,85,0.04)', padding: '12px 16px', borderLeft: `3px solid ${NAVY}`, margin: '0 0 16px', fontWeight: 500, overflowWrap: 'anywhere' }}
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}