From 7b639941b3e39327410a782fa07b4e2887f449a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20He=C3=9Fe?= Date: Fri, 14 Aug 2026 22:10:22 +0200 Subject: [PATCH] Added legal notes function --- server.ts | 24 ++ src/App.tsx | 9 +- src/components/AdminPanel.tsx | 596 +++++++++++++++++++++++++++- src/components/ClaraPortfolio.tsx | 40 +- src/components/EroticaPortfolio.tsx | 40 +- src/components/LegalModal.tsx | 199 ++++++++++ src/components/ReneePortfolio.tsx | 40 +- src/components/SciFiPortfolio.tsx | 38 +- src/defaultData.ts | 79 +++- src/types.ts | 10 + 10 files changed, 1055 insertions(+), 20 deletions(-) create mode 100644 src/components/LegalModal.tsx diff --git a/server.ts b/server.ts index f283b91..89f963d 100644 --- a/server.ts +++ b/server.ts @@ -73,6 +73,12 @@ async function initDatabase(): Promise { modified = true; } + // Ensure legalDocuments exists + if (!parsed.legalDocuments || !Array.isArray(parsed.legalDocuments)) { + parsed.legalDocuments = defaultAuthorData.legalDocuments || []; + modified = true; + } + if (modified) { await fs.writeFile(DATA_FILE, JSON.stringify(parsed, null, 2), "utf-8"); } @@ -177,6 +183,24 @@ app.post("/api/admin/save-profile", verifyToken, async (req, res) => { } }); +// 3b. Save legal documents (Impressum & Datenschutzerklärung) +app.post("/api/admin/save-legal", verifyToken, async (req, res) => { + const { legalDocuments } = req.body; + if (!Array.isArray(legalDocuments)) { + res.status(400).json({ error: "legalDocuments muss ein Array sein." }); + return; + } + + try { + dbCache.legalDocuments = legalDocuments; + await fs.writeFile(DATA_FILE, JSON.stringify(dbCache, null, 2), "utf-8"); + res.json({ success: true, message: "Rechtliche Dokumente erfolgreich gespeichert." }); + } catch (err: any) { + console.error("Failed to write legal documents to database.json:", err); + res.status(500).json({ error: "Fehler beim Speichern der rechtlichen Dokumente." }); + } +}); + // 4. Creative AI Blurb Assistant for book blurb updates app.post("/api/admin/generate-blurb", verifyToken, async (req, res) => { const { title, genre, ideas, tone } = req.body; diff --git a/src/App.tsx b/src/App.tsx index 4c4c836..d97e8c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -153,16 +153,17 @@ export default function App() { // Render active portfolio const renderActivePortfolio = () => { const activeSuite = getActiveSuite(); + const legalDocs = authorData.legalDocuments || []; if (activeSuite === "erotica") { - return ; + return ; } if (activeSuite === "clara") { - return ; + return ; } if (activeSuite === "renee") { - return ; + return ; } - return ; + return ; }; return ( diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index e59081c..8d4406d 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect, FormEvent } from "react"; -import { AuthorData, AuthorProfile, Book, Project } from "../types"; +import Markdown from "react-markdown"; +import { AuthorData, AuthorProfile, Book, Project, LegalDocument } from "../types"; import { Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles, BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight, - Palette, Type, Image as ImageIcon, Globe, Tag + Palette, Type, Image as ImageIcon, Globe, Tag, + Scale, ShieldCheck, FileText, CheckSquare, Square, Copy, ArrowRight } from "lucide-react"; import ImagePicker from "./ImagePicker"; @@ -24,8 +26,8 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { // Selected Profile state ("scifi" vs "erotica" vs "clara" vs "renee") const [activeProfile, setActiveProfile] = useState<"scifi" | "erotica" | "clara" | "renee">("scifi"); - // Sub-tab selection state ("profile" | "texts" | "design" | "books" | "projects") - const [activeTab, setActiveTab] = useState<"profile" | "texts" | "design" | "books" | "projects">("profile"); + // Sub-tab selection state ("profile" | "texts" | "design" | "books" | "projects" | "legal") + const [activeTab, setActiveTab] = useState<"profile" | "texts" | "design" | "books" | "projects" | "legal">("profile"); // Editing forms state const [editingBookId, setEditingBookId] = useState(null); @@ -35,6 +37,11 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { const [editingProjectId, setEditingProjectId] = useState(null); const [projectForm, setProjectForm] = useState>({}); + // Legal Documents state + const [editingLegalId, setEditingLegalId] = useState(null); + const [legalForm, setLegalForm] = useState>({}); + const [legalPreviewMode, setLegalPreviewMode] = useState<"edit" | "preview">("edit"); + // AI Prompt Tool State const [aiTitle, setAiTitle] = useState(""); const [aiGenre, setAiGenre] = useState(""); @@ -356,6 +363,120 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { } }; + // --- LEGAL DOCUMENTS MANAGEMENT --- + const handleSaveLegalDocument = async () => { + if (!authorData) return; + const currentDocs = authorData.legalDocuments || []; + let updatedDocs = [...currentDocs]; + const assigned: ("scifi" | "erotica" | "clara" | "renee")[] = + legalForm.assignedProfiles && legalForm.assignedProfiles.length > 0 + ? legalForm.assignedProfiles + : ["scifi", "erotica", "clara", "renee"]; + + if (editingLegalId === "new") { + const newDoc: LegalDocument = { + id: "legal_" + Date.now(), + title: legalForm.title || "Rechtliches Dokument", + type: legalForm.type || "impressum", + content: legalForm.content || "", + updatedAt: legalForm.updatedAt || new Date().toISOString().split("T")[0], + assignedProfiles: assigned + }; + updatedDocs.push(newDoc); + } else { + updatedDocs = updatedDocs.map(d => + d.id === editingLegalId + ? { + ...d, + ...legalForm, + assignedProfiles: assigned, + updatedAt: legalForm.updatedAt || new Date().toISOString().split("T")[0] + } as LegalDocument + : d + ); + } + + setLoading(true); + try { + const res = await fetch("/api/admin/save-legal", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ legalDocuments: updatedDocs }) + }); + if (res.ok) { + setAuthorData({ ...authorData, legalDocuments: updatedDocs }); + setEditingLegalId(null); + setLegalForm({}); + } else { + alert("Fehler beim Speichern des Dokuments."); + } + } catch { + alert("Netzwerkfehler beim Speichern."); + } finally { + setLoading(false); + } + }; + + const handleDeleteLegalDocument = async (docId: string) => { + if (!authorData || !confirm("Möchten Sie dieses rechtliche Dokument wirklich löschen?")) return; + const updatedDocs = (authorData.legalDocuments || []).filter(d => d.id !== docId); + setLoading(true); + try { + const res = await fetch("/api/admin/save-legal", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ legalDocuments: updatedDocs }) + }); + if (res.ok) { + setAuthorData({ ...authorData, legalDocuments: updatedDocs }); + if (editingLegalId === docId) { + setEditingLegalId(null); + setLegalForm({}); + } + } else { + alert("Fehler beim Löschen des Dokuments."); + } + } catch { + alert("Netzwerkfehler beim Löschen."); + } finally { + setLoading(false); + } + }; + + const handleDuplicateLegalDocument = (doc: LegalDocument) => { + setEditingLegalId("new"); + setLegalForm({ + title: `${doc.title} (Kopie)`, + type: doc.type, + content: doc.content, + updatedAt: new Date().toISOString().split("T")[0], + assignedProfiles: [...doc.assignedProfiles] + }); + }; + + const toggleProfileAssignment = (pKey: "scifi" | "erotica" | "clara" | "renee") => { + const current = legalForm.assignedProfiles || []; + if (current.includes(pKey)) { + setLegalForm({ ...legalForm, assignedProfiles: current.filter(k => k !== pKey) }); + } else { + setLegalForm({ ...legalForm, assignedProfiles: [...current, pKey] }); + } + }; + + const handleAssignAllProfiles = (assignAll: boolean) => { + if (assignAll) { + setLegalForm({ ...legalForm, assignedProfiles: ["scifi", "erotica", "clara", "renee"] }); + } else { + setLegalForm({ ...legalForm, assignedProfiles: [] }); + } + }; + if (!isLoggedIn) { return (
@@ -523,6 +644,13 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { Bücherregal ({profile.books.length}) +
@@ -1616,6 +1744,466 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { )} + {/* TAB: LEGAL DOCUMENTS (IMPRESSUM & DATENSCHUTZ) */} + {activeTab === "legal" && ( +
+ + {/* Header */} +
+
+

+ + Rechtliche Pflichtangaben & Dokumente +

+

+ Erstellen und verwalten Sie Impressum, Datenschutzerklärungen und individuelle Rechtsbelehrungen für Ihre Autorenprofile. +

+
+ + {!editingLegalId && ( + + )} +
+ + {/* Document List when not editing */} + {!editingLegalId ? ( +
+ {(!authorData.legalDocuments || authorData.legalDocuments.length === 0) ? ( +
+ +
+

Noch keine rechtlichen Dokumente angelegt

+

+ Legen Sie ein Impressum oder eine Datenschutzerklärung an, um rechtliche Transparenz auf Ihren Portfolioseiten zu gewährleisten. +

+
+ +
+ ) : ( +
+ {authorData.legalDocuments.map((doc) => { + const profileNamesMap = { + scifi: authorData.scifi?.name || "Daniel Hesse", + erotica: authorData.erotica?.name || "Annie Slone", + clara: authorData.clara?.name || "Clara Finch", + renee: authorData.renee?.name || "Renee Heart" + }; + + return ( +
+
+
+
+ {doc.type === "impressum" ? ( + + ) : doc.type === "privacy" ? ( + + ) : ( + + )} +
+
+
+

{doc.title}

+ + {doc.type === "impressum" ? "Impressum (§ 5 DDG)" : doc.type === "privacy" ? "Datenschutz (DSGVO)" : "Individuell"} + +
+ + Stand: {doc.updatedAt || "Aktuell"} + +
+
+ +
+ + + +
+
+ + {/* Assigned Profiles Badges */} +
+ Freigegeben für: + {(["scifi", "erotica", "clara", "renee"] as const).map((pKey) => { + const isAssigned = doc.assignedProfiles?.includes(pKey); + return ( + + {profileNamesMap[pKey]} + + ); + })} +
+ + {/* Short preview of content */} +
+ {doc.content ? doc.content.slice(0, 200) + (doc.content.length > 200 ? "..." : "") : "Kein Textinhalt hinterlegt."} +
+
+ ); + })} +
+ )} +
+ ) : ( + /* Document Editor Form */ +
+
+
+ +

+ {editingLegalId === "new" ? "Neues Rechtsdokument anlegen" : `Dokument bearbeiten: ${legalForm.title || ""}`} +

+
+ +
+ + +
+
+ + {/* Form fields */} +
+
+ + setLegalForm({ ...legalForm, title: e.target.value })} + placeholder="z.B. Impressum" + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" + /> +
+ +
+ + +
+ +
+ + setLegalForm({ ...legalForm, updatedAt: e.target.value })} + placeholder="z.B. 2026-08-14" + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm font-mono" + /> +
+
+ + {/* Profile Assignment Selection */} +
+
+
+
+ Freigabe für Autorenprofile & Seiten +
+

+ Wählen Sie aus, auf welchen Portfolioseiten dieses Dokument im Footer als Link eingeblendet wird. +

+
+ +
+ + +
+
+ +
+ {[ + { key: "scifi" as const, name: authorData.scifi?.name || "Daniel Hesse", badge: "Sci-Fi" }, + { key: "erotica" as const, name: authorData.erotica?.name || "Annie Slone", badge: "Erotik" }, + { key: "clara" as const, name: authorData.clara?.name || "Clara Finch", badge: "Naturlyrik" }, + { key: "renee" as const, name: authorData.renee?.name || "Renee Heart", badge: "Romantik" } + ].map((item) => { + const checked = legalForm.assignedProfiles?.includes(item.key); + return ( +
toggleProfileAssignment(item.key)} + className={`p-3 rounded-xl border transition-all cursor-pointer flex items-center justify-between gap-2 select-none ${ + checked + ? "bg-indigo-950/40 border-indigo-500/60 text-white" + : "bg-slate-900/60 border-slate-800 text-slate-400 hover:border-slate-700" + }`} + > +
+ {item.badge} + {item.name} +
+ {checked ? ( + + ) : ( + + )} +
+ ); + })} +
+
+ + {/* Text Editor or Preview */} + {legalPreviewMode === "edit" ? ( +
+
+ +
+ Mustertexte einfügen: + + +
+
+ +