Next.js 15 App Router mit öffentlicher Übersicht und geschütztem Admin-Bereich zur Pflege von Datenschutzerklärungen nach Art. 12ff. DSGVO. - Oberfläche auf Basis von shadcn/ui, Inter lokal eingebunden - Prisma/SQLite, Auth.js mit Credentials-Provider und Rollen - Massenimport der Merkblätter des Amtes Leezen aus PDF - Docker-Image (standalone) und Gitea-Workflow zur Veröffentlichung Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
"use client"
|
|
|
|
import { Printer } from "lucide-react"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
|
import { PrivacyPolicy } from "@/types/privacy-policy"
|
|
import { markdownToHtmlSync } from "@/lib/markdown"
|
|
|
|
interface PrintPolicyProps {
|
|
policy: PrivacyPolicy
|
|
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
|
|
size?: "default" | "sm" | "lg" | "icon" | "icon-sm"
|
|
label?: string
|
|
className?: string
|
|
}
|
|
|
|
const iconSizes = ["icon", "icon-sm"]
|
|
|
|
export function PrintPolicy({
|
|
policy,
|
|
variant = "outline",
|
|
size = "sm",
|
|
label = "Drucken",
|
|
className,
|
|
}: PrintPolicyProps) {
|
|
const handlePrint = () => {
|
|
const printWindow = window.open("", "_blank")
|
|
if (!printWindow) return
|
|
|
|
printWindow.document.write(generatePrintContent(policy))
|
|
printWindow.document.close()
|
|
}
|
|
|
|
const isIconOnly = iconSizes.includes(size)
|
|
|
|
const button = (
|
|
<Button variant={variant} size={size} onClick={handlePrint} className={className}>
|
|
<Printer />
|
|
{!isIconOnly && label}
|
|
</Button>
|
|
)
|
|
|
|
if (!isIconOnly) return button
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
<TooltipContent>Als PDF drucken</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
function generatePrintContent(policy: PrivacyPolicy): string {
|
|
const formatDate = (dateString: string) =>
|
|
new Date(dateString).toLocaleDateString("de-DE", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
})
|
|
|
|
const cleanMarkdown = (content: string) => markdownToHtmlSync(content)
|
|
|
|
const section = (title: string, content?: string | null) =>
|
|
content
|
|
? `<section class="section">
|
|
<h2 class="section-title">${title}</h2>
|
|
<div class="section-content">${cleanMarkdown(content)}</div>
|
|
</section>`
|
|
: ""
|
|
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${policy.title}</title>
|
|
<style>
|
|
@page { margin: 2cm 1.8cm; size: A4; }
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, "Segoe UI", Arial, sans-serif;
|
|
font-size: 10pt;
|
|
line-height: 1.5;
|
|
color: #18181b;
|
|
margin: 0;
|
|
}
|
|
.header {
|
|
border-bottom: 1px solid #d4d4d8;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 18px;
|
|
page-break-after: avoid;
|
|
}
|
|
.eyebrow {
|
|
font-size: 7.5pt;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: #71717a;
|
|
margin: 0 0 4px 0;
|
|
}
|
|
.header h1 {
|
|
margin: 0 0 4px 0;
|
|
font-size: 17pt;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
line-height: 1.2;
|
|
}
|
|
.meta-info { font-size: 8pt; color: #71717a; margin: 0; }
|
|
.section { margin-bottom: 14px; page-break-inside: avoid; orphans: 3; widows: 3; }
|
|
.section-title {
|
|
font-size: 9pt;
|
|
font-weight: 600;
|
|
letter-spacing: .04em;
|
|
text-transform: uppercase;
|
|
color: #52525b;
|
|
margin: 0 0 5px 0;
|
|
padding-bottom: 3px;
|
|
border-bottom: 1px solid #e4e4e7;
|
|
page-break-after: avoid;
|
|
}
|
|
.section-content { font-size: 9.5pt; }
|
|
.section-content > :first-child { margin-top: 0; }
|
|
.section-content > :last-child { margin-bottom: 0; }
|
|
.section-content p { margin: 0 0 5px 0; }
|
|
.section-content h1, .section-content h2, .section-content h3 {
|
|
font-size: 9.5pt; font-weight: 600; margin: 8px 0 3px 0;
|
|
}
|
|
.section-content strong { font-weight: 600; }
|
|
.section-content code {
|
|
background: #f4f4f5; padding: 1px 3px; border-radius: 2px;
|
|
font-family: ui-monospace, "Courier New", monospace; font-size: 8.5pt;
|
|
}
|
|
.section-content ul, .section-content ol { margin: 4px 0; padding-left: 16px; }
|
|
.section-content li { margin: 2px 0; }
|
|
.section-content a { color: #2563eb; text-decoration: none; }
|
|
.footer {
|
|
border-top: 1px solid #e4e4e7;
|
|
margin-top: 24px;
|
|
padding-top: 8px;
|
|
font-size: 7.5pt;
|
|
color: #a1a1aa;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
page-break-inside: avoid;
|
|
}
|
|
@media print {
|
|
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<p class="eyebrow">Syncova Policies · Datenschutzerklärung</p>
|
|
<h1>${policy.title}</h1>
|
|
<p class="meta-info">
|
|
Erstellt am ${formatDate(policy.createdAt)}${
|
|
policy.updatedAt !== policy.createdAt
|
|
? ` · Aktualisiert am ${formatDate(policy.updatedAt)}`
|
|
: ""
|
|
}
|
|
</p>
|
|
</header>
|
|
|
|
${section("Verantwortlicher im Sinne der DSGVO", policy.responsible)}
|
|
${section("Kontaktdaten des Datenschutzbeauftragten", policy.contactDPO)}
|
|
${section("Bezeichnung der Verarbeitungstätigkeit", policy.processingDescription)}
|
|
${section("Zweck der Datenerhebung", policy.processingPurposes)}
|
|
${section("Rechtsgrundlage der Datenerhebung", policy.legalBasis)}
|
|
${section("Geplante Empfänger der Daten", policy.recipients)}
|
|
${section("Speicherdauer der Daten", policy.storageDuration)}
|
|
${section("Herkunft der Daten", policy.dataSource)}
|
|
${section("Betroffenen-Rechte", policy.dataSubjectsRights)}
|
|
${section("Beschwerderecht bei der Aufsichtsbehörde", policy.complaintRight)}
|
|
${section("Widerrufsrecht bei Einwilligung", policy.withdrawalRight)}
|
|
${section("Ihre Pflicht zur Bereitstellung der Daten", policy.provisionObligation)}
|
|
${section("Folgen, wenn Sie die Daten nicht angeben", policy.provisionConsequences)}
|
|
|
|
<footer class="footer">
|
|
<span>${policy.title}</span>
|
|
<span>Gedruckt am ${new Date().toLocaleDateString("de-DE", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}</span>
|
|
</footer>
|
|
|
|
<script>
|
|
window.onload = function () {
|
|
setTimeout(function () { window.print(); }, 250);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`
|
|
}
|