From 3abaaba86913e0283b49dbf5fdc9a87b2ef5e2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20He=C3=9Fe?= Date: Fri, 14 Aug 2026 23:40:58 +0200 Subject: [PATCH] feat: require consent for Spotify embeds --- server.ts | 33 +- src/components/AdminPanel.tsx | 398 ++++++++++++------------ src/components/ClaraPortfolio.tsx | 133 ++++---- src/components/EroticaPortfolio.tsx | 133 ++++---- src/components/ProjectDetailMedia.tsx | 36 +-- src/components/ReneePortfolio.tsx | 147 ++++----- src/components/SciFiPortfolio.tsx | 146 ++++----- src/components/SpotifyConsentPlayer.tsx | 68 ++++ src/components/SpotlightSection.tsx | 78 ++--- src/defaultData.ts | 24 +- 10 files changed, 597 insertions(+), 599 deletions(-) create mode 100644 src/components/SpotifyConsentPlayer.tsx diff --git a/server.ts b/server.ts index 8110f32..5c8d5b2 100644 --- a/server.ts +++ b/server.ts @@ -15,7 +15,22 @@ const PORT = 3000; const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(process.cwd(), "data")); const DATA_FILE = path.join(DATA_DIR, "database.json"); const BACKUP_DIR = path.join(DATA_DIR, "backups"); -const CURRENT_SCHEMA_VERSION = 1; +const CURRENT_SCHEMA_VERSION = 2; +const LEGACY_DEFAULT_IMAGE_URLS = new Set([ + "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300", + "https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300", + "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300", + "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=300", + "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&q=80&w=600", + "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400", +]); let writeQueue: Promise = Promise.resolve(); let serverReady = false; @@ -83,6 +98,7 @@ async function initDatabase(): Promise { } // Legacy files may not contain all newer profiles yet; migrations below add them. const parsed = rawParsed as AuthorData; + const loadedSchemaVersion = parsed.schemaVersion ?? 0; let modified = false; // Ensure all dynamic static text and design properties exist for all profiles @@ -129,7 +145,20 @@ async function initDatabase(): Promise { modified = true; } - if (parsed.schemaVersion === undefined) { + // Version 2 removes only the exact historical demo images. User-uploaded + // and other explicitly configured image URLs remain untouched. + if (loadedSchemaVersion < 2) { + for (const key of profileKeys) { + const profile = parsed[key]; + if (LEGACY_DEFAULT_IMAGE_URLS.has(profile.avatarUrl)) profile.avatarUrl = ""; + for (const book of profile.books) { + if (LEGACY_DEFAULT_IMAGE_URLS.has(book.coverUrl)) book.coverUrl = ""; + } + } + modified = true; + } + + if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) { parsed.schemaVersion = CURRENT_SCHEMA_VERSION; modified = true; } diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index 82dd123..279d85b 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect, FormEvent } from "react"; import Markdown from "react-markdown"; import { AuthorData, AuthorProfile, Book, Project, LegalDocument } from "../types"; -import { - Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles, +import { + Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles, BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight, Palette, Type, Image as ImageIcon, Globe, Tag, Scale, ShieldCheck, FileText, CheckSquare, Square, Copy, ArrowRight @@ -22,10 +22,10 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { // Author Data State const [authorData, setAuthorData] = useState(null); - + // 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" | "legal") const [activeTab, setActiveTab] = useState<"profile" | "texts" | "design" | "books" | "projects" | "legal">("profile"); @@ -33,7 +33,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { const [editingBookId, setEditingBookId] = useState(null); const [bookForm, setBookForm] = useState>({}); const [genreInputRaw, setGenreInputRaw] = useState(""); - + const [editingProjectId, setEditingProjectId] = useState(null); const [projectForm, setProjectForm] = useState>({}); @@ -111,20 +111,20 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { if (!authorData) return; setError(""); setLoading(true); - + try { const res = await fetch("/api/admin/save-profile", { method: "POST", - headers: { + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, - body: JSON.stringify({ - profileKey, - profileData: authorData[profileKey] + body: JSON.stringify({ + profileKey, + profileData: authorData[profileKey] }), }); - + const resData = await res.json(); if (res.ok && resData.success) { alert("Profilinformationen erfolgreich gespeichert!"); @@ -155,7 +155,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { // --- PROJECT ACTIONS --- const handleSaveProject = async () => { if (!authorData || !profile) return; - + let updatedProjects = [...profile.projects]; if (editingProjectId === "new") { const newProj: Project = { @@ -169,20 +169,20 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { }; updatedProjects.push(newProj); } else { - updatedProjects = updatedProjects.map(p => + updatedProjects = updatedProjects.map(p => p.id === editingProjectId ? { ...p, ...projectForm } : p ); } const updatedProfile = { ...profile, projects: updatedProjects }; const nextData = { ...authorData, [activeProfile]: updatedProfile }; - + // Save to server setLoading(true); try { const res = await fetch("/api/admin/save-profile", { method: "POST", - headers: { + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, @@ -204,7 +204,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { const handleDeleteProject = async (projId: string) => { if (!authorData || !profile || !confirm("Möchten Sie dieses Projekt wirklich löschen?")) return; - + const updatedProjects = profile.projects.filter(p => p.id !== projId); const updatedProfile = { ...profile, projects: updatedProjects }; const nextData = { ...authorData, [activeProfile]: updatedProfile }; @@ -213,7 +213,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { try { const res = await fetch("/api/admin/save-profile", { method: "POST", - headers: { + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, @@ -234,7 +234,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { // --- BOOK ACTIONS --- const handleSaveBook = async () => { if (!authorData || !profile) return; - + const parsedGenres = genreInputRaw.split(",").map(g => g.trim()).filter(Boolean); let updatedBooks = [...profile.books]; @@ -248,7 +248,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { id: "book_" + Date.now(), title: bookForm.title || "Unbenanntes Buch", subtitle: bookForm.subtitle || "", - coverUrl: bookForm.coverUrl || "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400", + coverUrl: bookForm.coverUrl || "", description: bookForm.description || "", publishedYear: bookForm.publishedYear || new Date().getFullYear(), buyLink: bookForm.buyLink || "", @@ -259,10 +259,10 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { }; updatedBooks.push(newBook); } else { - updatedBooks = updatedBooks.map(b => - b.id === editingBookId ? { - ...b, - ...bookForm, + updatedBooks = updatedBooks.map(b => + b.id === editingBookId ? { + ...b, + ...bookForm, genres: parsedGenres, isSpotlight: !!bookForm.isSpotlight, spotlightBadge: bookForm.spotlightBadge || "" @@ -277,7 +277,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { try { const res = await fetch("/api/admin/save-profile", { method: "POST", - headers: { + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, @@ -299,7 +299,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { const handleDeleteBook = async (bookId: string) => { if (!authorData || !profile || !confirm("Möchten Sie dieses Buch aus dem Bücherregal löschen?")) return; - + const updatedBooks = profile.books.filter(b => b.id !== bookId); const updatedProfile = { ...profile, books: updatedBooks }; const nextData = { ...authorData, [activeProfile]: updatedProfile }; @@ -308,7 +308,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { try { const res = await fetch("/api/admin/save-profile", { method: "POST", - headers: { + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, @@ -337,7 +337,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { try { const res = await fetch("/api/admin/generate-blurb", { method: "POST", - headers: { + headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` }, @@ -370,7 +370,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { if (!authorData) return; const currentDocs = authorData.legalDocuments || []; let updatedDocs = [...currentDocs]; - const assigned: ("scifi" | "erotica" | "clara" | "renee")[] = + const assigned: ("scifi" | "erotica" | "clara" | "renee")[] = legalForm.assignedProfiles && legalForm.assignedProfiles.length > 0 ? legalForm.assignedProfiles : ["scifi", "erotica", "clara", "renee"]; @@ -386,14 +386,14 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { }; updatedDocs.push(newDoc); } else { - updatedDocs = updatedDocs.map(d => - d.id === editingLegalId - ? { - ...d, - ...legalForm, + updatedDocs = updatedDocs.map(d => + d.id === editingLegalId + ? { + ...d, + ...legalForm, assignedProfiles: assigned, updatedAt: legalForm.updatedAt || new Date().toISOString().split("T")[0] - } as LegalDocument + } as LegalDocument : d ); } @@ -494,8 +494,8 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
- setPassword(e.target.value)} placeholder="Passwort eingeben..." @@ -511,8 +511,8 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
)} - - - - - - - - - -