feat: manage datenschutz page via payload
This commit is contained in:
@@ -1,198 +1,6 @@
|
||||
import React from 'react'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
|
||||
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 ANTHRA = '#1A2030'
|
||||
const NAVY = '#111D55'
|
||||
const GOLD = '#EFBF04'
|
||||
const CREAM = '#EFE5E3'
|
||||
const FF = '"IBM Plex Sans", sans-serif'
|
||||
const FB = '"Inter", sans-serif'
|
||||
|
||||
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')
|
||||
import LegalPage from '@/spa/components/legal/LegalPage'
|
||||
import { impressumContent } from '@/spa/impressumContent'
|
||||
|
||||
export default function Impressum() {
|
||||
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' }}>
|
||||
<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)')}
|
||||
>
|
||||
<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 }}>{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' }}>
|
||||
{fallbackText(hero.heading, impressumContent.hero.heading)}
|
||||
</h1>
|
||||
<p
|
||||
style={{ fontFamily: FB, fontSize: 15, color: 'rgba(255,255,255,0.45)', margin: 0 }}
|
||||
dangerouslySetInnerHTML={{ __html: fallbackText(hero.descriptionHtml, impressumContent.hero.descriptionHtml) }}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<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' }}>
|
||||
{!isMobile && (
|
||||
<nav style={{ position: 'sticky', top: 80 }}>
|
||||
<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 }}>
|
||||
{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.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={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>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 56 }}>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
const body: React.CSSProperties = {
|
||||
fontFamily: '"Inter", sans-serif',
|
||||
fontSize: 15,
|
||||
color: 'rgba(58,58,58,0.82)',
|
||||
lineHeight: 1.85,
|
||||
margin: '0 0 16px',
|
||||
overflowWrap: 'anywhere',
|
||||
}
|
||||
|
||||
function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div id={id} style={{ scrollMarginTop: 80 }}>
|
||||
<div style={{ borderLeft: `3px solid ${GOLD}`, paddingLeft: 16, marginBottom: 28 }}>
|
||||
<h2 style={{ fontFamily: FF, fontSize: 14, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.2em', color: NAVY, margin: 0 }}>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
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 }) {
|
||||
return (
|
||||
<div style={{ marginBottom: 28 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 10 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: GOLD, letterSpacing: '0.12em' }}>{num}.</span>
|
||||
<h3 style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: NAVY, margin: 0 }}>{title}</h3>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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' }}>
|
||||
{lines.map((line, index) => (
|
||||
<React.Fragment key={`${line}-${index}`}>
|
||||
{line}
|
||||
{index < lines.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</address>
|
||||
)
|
||||
}
|
||||
|
||||
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' }}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
)
|
||||
return <LegalPage content={impressumContent} cmsKey="impressum" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user