shinkan-jinkendo/frontend/src/pages/LegalPage.jsx

334 lines
11 KiB
React
Raw Normal View History

import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { jsPDF } from 'jspdf'
import api from '../utils/api'
function generateLegalPdf(doc) {
const pdf = new jsPDF({ format: 'a4', unit: 'mm' })
const marginL = 22, marginR = 22, marginTop = 28, pageW = 210
const contentW = pageW - marginL - marginR
const bottomLimit = 277
let y = marginTop
const checkBreak = (need) => {
if (y + need > bottomLimit) { pdf.addPage(); y = marginTop }
}
pdf.setFont('helvetica', 'bold')
pdf.setFontSize(20)
pdf.text(doc.title, marginL, y)
y += 10
const dateStr = doc.published_at
? new Date(doc.published_at).toLocaleDateString('de-DE')
: new Date(doc.updated_at || doc.created_at).toLocaleDateString('de-DE')
const metaLine = `Version ${doc.version} | Gueltig seit ${dateStr}`
pdf.setFont('helvetica', 'normal')
pdf.setFontSize(10)
pdf.setTextColor(90, 90, 90)
pdf.text(metaLine, marginL, y)
y += 3
pdf.setDrawColor(0, 0, 0)
pdf.setLineWidth(0.4)
pdf.line(marginL, y, pageW - marginR, y)
y += 8
pdf.setTextColor(0, 0, 0)
for (const section of (doc.content_sections || [])) {
checkBreak(14)
pdf.setFont('helvetica', 'bold')
pdf.setFontSize(11)
pdf.text(section.heading || '', marginL, y)
y += 6
if (section.content) {
pdf.setFont('helvetica', 'normal')
pdf.setFontSize(10)
const lines = pdf.splitTextToSize(section.content, contentW)
for (const line of lines) {
checkBreak(5)
pdf.text(line, marginL, y)
y += 5
}
}
y += 5
}
const total = pdf.getNumberOfPages()
for (let i = 1; i <= total; i++) {
pdf.setPage(i)
pdf.setFont('helvetica', 'normal')
pdf.setFontSize(8)
pdf.setTextColor(150, 150, 150)
const fy = 289
pdf.text(
`Shinkan Jinkendo | Exportiert am ${new Date().toLocaleDateString('de-DE')}`,
marginL, fy
)
pdf.text(`Seite ${i} von ${total}`, pageW - marginR, fy, { align: 'right' })
}
pdf.save(`${doc.document_type}_v${doc.version}.pdf`)
}
// document_type values used in the DB / API
const TYPE_MAP = {
impressum: 'impressum',
datenschutz: 'privacy_policy',
nutzungsbedingungen: 'terms_of_use',
medienrichtlinie: 'media_policy',
}
const PAGES = {
impressum: {
title: 'Impressum',
sections: [
{
heading: 'Betreiber / Verantwortlicher',
placeholder: '[Name und Rechtsform des Betreibers — vom Betreiber einzutragen]',
},
{
heading: 'Anschrift',
placeholder: '[Straße, Hausnummer, PLZ, Ort — vom Betreiber einzutragen]',
},
{
heading: 'Vertretungsberechtigte Person',
placeholder: '[Name der vertretungsberechtigten Person — vom Betreiber einzutragen]',
},
{
heading: 'Kontakt',
placeholder: '[E-Mail-Adresse, ggf. Telefonnummer — vom Betreiber einzutragen]',
},
{
heading: 'Registerangaben (falls relevant)',
placeholder: '[Vereinsregister, Handelsregister o. ä. — vom Rechtsanwalt prüfen lassen]',
},
],
},
datenschutz: {
title: 'Datenschutzerklärung',
sections: [
{
heading: 'Verantwortlicher',
placeholder: '[Name, Anschrift und Kontakt des Verantwortlichen — vom Betreiber einzutragen]',
},
{
heading: 'Zwecke der Verarbeitung',
placeholder: '[Welche Daten werden zu welchem Zweck verarbeitet? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Rechtsgrundlagen',
placeholder: '[Art. 6 DSGVO: Einwilligung, Vertrag, berechtigtes Interesse — vom Rechtsanwalt zu bestimmen]',
},
{
heading: 'Empfänger und Dienstleister',
placeholder: '[SMTP-Anbieter, Hosting, ggf. weitere — vom Rechtsanwalt und Betreiber zu listen]',
},
{
heading: 'Speicherdauern',
placeholder: '[Wie lange werden welche Daten gespeichert? — vom Rechtsanwalt zu bestimmen]',
},
{
heading: 'Betroffenenrechte',
placeholder: '[Auskunft, Berichtigung, Löschung, Widerspruch, Datenübertragbarkeit — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Browser-Speicher (localStorage, sessionStorage)',
placeholder: '[Technisch notwendige Speicherung des Auth-Tokens und Sitzungsdaten. Nach TDDDG §25 ggf. ohne Einwilligung zulässig — vom Rechtsanwalt zu prüfen]',
},
{
heading: 'Kontakt für Datenschutzanfragen',
placeholder: '[E-Mail oder Kontaktweg für Datenschutzanfragen — vom Betreiber einzutragen]',
},
{
heading: 'Zuständige Aufsichtsbehörde',
placeholder: '[Zuständige Datenschutzbehörde je nach Bundesland — vom Rechtsanwalt zu bestimmen]',
},
],
},
nutzungsbedingungen: {
title: 'Nutzungsbedingungen',
sections: [
{
heading: 'Nutzungsumfang',
placeholder: '[Für welche Nutzergruppen und Zwecke darf die Plattform genutzt werden? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Registrierung',
placeholder: '[Voraussetzungen für die Registrierung, Wahrheitspflicht der Angaben — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Zulässige und unzulässige Inhalte',
placeholder: '[Was darf hochgeladen und veröffentlicht werden? Was ist verboten? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Verantwortlichkeit der Nutzer',
placeholder: '[Nutzer sind für ihre hochgeladenen Inhalte selbst verantwortlich — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Sperrung und Löschung',
placeholder: '[Unter welchen Bedingungen können Konten oder Inhalte gesperrt oder gelöscht werden? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Haftungshinweise',
placeholder: '[Haftungsausschluss für Nutzerinhalte, externe Links, Systemausfälle — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Geltungsbereich und anwendbares Recht',
placeholder: '[Welches Recht ist anwendbar? Welcher Gerichtsstand gilt? — vom Rechtsanwalt zu bestimmen]',
},
],
},
medienrichtlinie: {
title: 'Medienrichtlinie',
sections: [
{
heading: 'Urheberrechte',
placeholder: '[Nur eigene oder ausdrücklich lizenzierte Inhalte hochladen — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Rechte am eigenen Bild (§ 22 KUG)',
placeholder: '[Erkennbare Personen müssen eingewilligt haben; besondere Regeln für Minderjährige — juristisch zu prüfen und zu formulieren]',
},
{
heading: 'Minderjährige',
placeholder: '[Besondere Schutzpflichten bei Aufnahmen von Minderjährigen — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Musik und sonstige Fremdinhalte',
placeholder: '[Keine Hintergrundmusik oder andere Fremdinhalte ohne gültige Lizenz — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Sichtbarkeitsstufen',
placeholder: '[Erläuterung der Stufen: privat, vereinsintern, öffentlich — vom Betreiber zu beschreiben]',
},
{
heading: 'Meldewege für rechtsverletzende Inhalte',
placeholder: '[Wie können Inhalte gemeldet werden? — wird nach Umsetzung von P-13 (Content-Melde-Backend) ergänzt]',
},
{
heading: 'Lösch- und Sperrlogik',
placeholder: '[Wann und wie werden gemeldete Inhalte entfernt oder gesperrt? — wird nach Umsetzung von P-11 und P-13 ergänzt]',
},
],
},
}
const LEGAL_LINKS = [
{ to: '/impressum', label: 'Impressum' },
{ to: '/datenschutz', label: 'Datenschutz' },
{ to: '/nutzungsbedingungen', label: 'Nutzungsbedingungen' },
{ to: '/medienrichtlinie', label: 'Medienrichtlinie' },
]
function LegalPage({ type }) {
const fallback = PAGES[type]
const [apiDoc, setApiDoc] = useState(undefined) // undefined = loading, null = not found
const [loading, setLoading] = useState(true)
const documentType = TYPE_MAP[type]
useEffect(() => {
if (!documentType) {
setLoading(false)
return
}
api.getPublishedLegalDocument(documentType)
.then(doc => setApiDoc(doc))
.catch(() => setApiDoc(null))
.finally(() => setLoading(false))
}, [documentType])
if (!fallback) return null
const isPlaceholder = !apiDoc
const title = apiDoc ? apiDoc.title : fallback.title
const sections = apiDoc
? (apiDoc.content_sections || [])
: fallback.sections.map(s => ({ heading: s.heading, content: s.placeholder }))
return (
<div style={{ minHeight: '100vh', background: 'var(--bg)', padding: '2rem 1rem' }}>
<div style={{ maxWidth: '720px', margin: '0 auto' }}>
<div style={{ marginBottom: '1.5rem' }}>
<Link to="/login" style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: '0.9rem' }}>
← Zurück zur Anmeldung
</Link>
</div>
{loading ? (
<div className="spinner" />
) : (
<>
{isPlaceholder && (
<div
className="card"
style={{
marginBottom: '1.5rem',
borderLeft: '4px solid var(--danger)',
background: 'var(--surface)',
}}
>
<strong style={{ color: 'var(--danger)' }}>⚠ MUSTER / PLATZHALTER</strong>
<p style={{ margin: '0.5rem 0 0', color: 'var(--text2)', fontSize: '0.9rem' }}>
Inhalt wird vor Produktivbetrieb juristisch geprüft und durch den Betreiber ergänzt.
Diese Seite hat keinen rechtlich verbindlichen Charakter.
</p>
</div>
)}
<div style={{ display: 'flex', alignItems: 'baseline', gap: '1rem', marginBottom: '2rem', flexWrap: 'wrap' }}>
<h1 style={{ margin: 0, color: 'var(--text1)' }}>{title}</h1>
{apiDoc && (
<button
onClick={() => generateLegalPdf(apiDoc)}
className="btn btn-secondary"
style={{ fontSize: '0.82rem', padding: '4px 12px', flexShrink: 0 }}
>
PDF herunterladen
</button>
)}
</div>
{sections.map((section, i) => (
<div key={i} style={{ marginBottom: '1.75rem' }}>
<h2 style={{ fontSize: '1.05rem', marginBottom: '0.4rem', color: 'var(--text1)' }}>
{section.heading}
</h2>
<p style={{ color: isPlaceholder ? 'var(--text3)' : 'var(--text1)', fontStyle: isPlaceholder ? 'italic' : 'normal', margin: 0, whiteSpace: 'pre-wrap' }}>
{section.content}
</p>
</div>
))}
</>
)}
<div
style={{
marginTop: '3rem',
paddingTop: '1rem',
borderTop: '1px solid var(--border)',
fontSize: '0.82rem',
color: 'var(--text3)',
textAlign: 'center',
display: 'flex',
gap: '1.25rem',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{LEGAL_LINKS.map((l) => (
<Link key={l.to} to={l.to} style={{ color: 'var(--text3)' }}>
{l.label}
</Link>
))}
</div>
</div>
</div>
)
}
export default LegalPage