import React, { useState, useEffect, FormEvent } from "react"; import Markdown from "react-markdown"; import { AuthorData, AuthorProfile, Book, Project, LegalDocument } from "../types"; import { 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 } from "lucide-react"; import ImagePicker from "./ImagePicker"; import AdminLogin from "./admin/AdminLogin"; import { adminApi, AdminApiError } from "../api/adminApi"; interface AdminPanelProps { onLogout: () => void; } export default function AdminPanel({ onLogout }: AdminPanelProps) { const [password, setPassword] = useState(""); const [isLoggedIn, setIsLoggedIn] = useState(false); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [notice, setNotice] = useState<{ message: string; tone: "success" | "error" } | null>(null); // 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"); // Editing forms state const [editingBookId, setEditingBookId] = useState(null); const [bookForm, setBookForm] = useState>({}); const [genreInputRaw, setGenreInputRaw] = useState(""); 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(""); const [aiIdeas, setAiIdeas] = useState(""); const [aiTone, setAiTone] = useState("Geheimnisvoll"); const [aiLoading, setAiLoading] = useState(false); const [aiResult, setAiResult] = useState(""); // Restore a valid server-side session on component mount. useEffect(() => { // Remove the legacy value that used to contain the admin password. localStorage.removeItem("author_admin_token"); const restoreSession = async () => { setLoading(true); try { await adminApi.session(); setIsLoggedIn(true); await fetchAuthorData(); } catch (err) { console.error("Admin session check failed:", err); } finally { setLoading(false); } }; restoreSession(); }, []); useEffect(() => { const handleExpiredSession = () => { setIsLoggedIn(false); setAuthorData(null); setError("Die Admin-Sitzung ist abgelaufen. Bitte erneut anmelden."); }; window.addEventListener("admin-session-expired", handleExpiredSession); return () => window.removeEventListener("admin-session-expired", handleExpiredSession); }, []); const fetchAuthorData = async () => { setLoading(true); try { const data = await adminApi.loadData(); setAuthorData(data); } catch (err) { console.error("Error fetching author data:", err); } finally { setLoading(false); } }; const handleLogin = async (e: FormEvent) => { e.preventDefault(); setError(""); setLoading(true); try { await adminApi.login(password); setPassword(""); setIsLoggedIn(true); await fetchAuthorData(); } catch (err) { setError(err instanceof AdminApiError ? err.message : "Verbindung zum Server fehlgeschlagen."); } finally { setLoading(false); } }; const handleLogoutLocal = async () => { try { await adminApi.logout(); } catch (err) { console.error("Admin logout failed:", err); } setIsLoggedIn(false); setAuthorData(null); onLogout(); }; const persistProfile = async (profileKey: "scifi" | "erotica" | "clara" | "renee", profileData: AuthorProfile) => { if (!authorData) throw new Error("Keine Profildaten geladen."); const result = await adminApi.saveProfile(profileKey, profileData, authorData.revision ?? 0); setAuthorData((current) => current ? { ...current, [profileKey]: profileData, revision: result.revision } : current); return result; }; const persistLegalDocuments = async (legalDocuments: LegalDocument[]) => { if (!authorData) throw new Error("Keine Profildaten geladen."); const result = await adminApi.saveLegal(legalDocuments, authorData.revision ?? 0); setAuthorData((current) => current ? { ...current, legalDocuments, revision: result.revision } : current); return result; }; const showNotice = (message: string, tone: "success" | "error" = "error") => setNotice({ message, tone }); useEffect(() => { if (!notice) return; const timeout = window.setTimeout(() => setNotice(null), 5000); return () => window.clearTimeout(timeout); }, [notice]); // Profile save handler const handleSaveProfileInfo = async (profileKey: "scifi" | "erotica" | "clara" | "renee") => { if (!authorData) return; setError(""); setLoading(true); try { await persistProfile(profileKey, authorData[profileKey]); showNotice("Profilinformationen erfolgreich gespeichert!", "success"); } catch (err) { setError(err instanceof AdminApiError ? err.message : "Fehler beim Senden der Profildaten."); } finally { setLoading(false); } }; // Helper properties to access the selected Profile object const profile = authorData ? authorData[activeProfile] : null; // --- Profile state updaters --- const updateProfileField = (field: keyof AuthorProfile, value: any) => { if (!authorData || !profile) return; const updatedProfile = { ...profile, [field]: value }; setAuthorData({ ...authorData, [activeProfile]: updatedProfile }); }; const updateCustomSectionLink = (index: number, field: "label" | "url", value: string) => { if (!profile) return; const links = Array.from({ length: 3 }, (_, linkIndex) => ({ label: profile.customSectionLinks?.[linkIndex]?.label || "", url: profile.customSectionLinks?.[linkIndex]?.url || "", })); links[index] = { ...links[index], [field]: value }; updateProfileField("customSectionLinks", links); }; // --- PROJECT ACTIONS --- const handleSaveProject = async () => { if (!authorData || !profile) return; let updatedProjects = [...profile.projects]; if (editingProjectId === "new") { const newProj: Project = { id: "proj_" + Date.now(), title: projectForm.title || "Unbenanntes Projekt", status: projectForm.status || "Konzept", progress: projectForm.progress || 0, description: projectForm.description || "", imageUrl: projectForm.imageUrl || "", spotifyPlaylistId: projectForm.spotifyPlaylistId || "" }; updatedProjects.push(newProj); } else { updatedProjects = updatedProjects.map(p => p.id === editingProjectId ? { ...p, ...projectForm } : p ); } const updatedProfile = { ...profile, projects: updatedProjects }; // Save to server setLoading(true); try { await persistProfile(activeProfile, updatedProfile); setEditingProjectId(null); setProjectForm({}); } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "Netzwerkfehler."); } finally { setLoading(false); } }; 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 }; setLoading(true); try { await persistProfile(activeProfile, updatedProfile); } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "Netzwerkfehler."); } finally { setLoading(false); } }; // --- BOOK ACTIONS --- const handleSaveBook = async () => { if (!authorData || !profile) return; const parsedGenres = genreInputRaw.split(",").map(g => g.trim()).filter(Boolean); let updatedBooks = [...profile.books]; // If current book is marked as spotlight, clear spotlight flag on all other books if (bookForm.isSpotlight) { updatedBooks = updatedBooks.map(b => ({ ...b, isSpotlight: false })); } if (editingBookId === "new") { const newBook: Book = { id: "book_" + Date.now(), title: bookForm.title || "Unbenanntes Buch", subtitle: bookForm.subtitle || "", coverUrl: bookForm.coverUrl || "", description: bookForm.description || "", publishedYear: bookForm.publishedYear || new Date().getFullYear(), buyLink: bookForm.buyLink || "", spotifyPlaylistId: bookForm.spotifyPlaylistId || "", genres: parsedGenres, isSpotlight: !!bookForm.isSpotlight, spotlightBadge: bookForm.spotlightBadge || "" }; updatedBooks.push(newBook); } else { updatedBooks = updatedBooks.map(b => b.id === editingBookId ? { ...b, ...bookForm, genres: parsedGenres, isSpotlight: !!bookForm.isSpotlight, spotlightBadge: bookForm.spotlightBadge || "" } as Book : b ); } const updatedProfile = { ...profile, books: updatedBooks }; setLoading(true); try { await persistProfile(activeProfile, updatedProfile); setEditingBookId(null); setBookForm({}); } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "Netzwerkfehler."); } finally { setLoading(false); } }; 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 }; setLoading(true); try { await persistProfile(activeProfile, updatedProfile); } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "Netzwerkfehler."); } finally { setLoading(false); } }; // --- GEMINI SYNOPSIS GENERATOR --- const handleGenerateAISynopsis = async () => { if (!aiTitle.trim()) { showNotice("Bitte geben Sie einen Buchtitel für den KI-Assistenten an."); return; } setAiLoading(true); setAiResult(""); try { const data = await adminApi.generateBlurb({ title: aiTitle, genre: aiGenre || (activeProfile === "scifi" ? "Science-Fiction" : "Literatur"), ideas: aiIdeas, tone: aiTone, }); setAiResult(data.text); setBookForm({ ...bookForm, description: data.text }); } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "Fehler bei der Verbindung zur KI-Schnittstelle."); } finally { setAiLoading(false); } }; // --- 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 { await persistLegalDocuments(updatedDocs); setEditingLegalId(null); setLegalForm({}); } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "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 { await persistLegalDocuments(updatedDocs); if (editingLegalId === docId) { setEditingLegalId(null); setLegalForm({}); } } catch (err) { showNotice(err instanceof AdminApiError ? err.message : "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 ; } if (!authorData || !profile) { return (
Lade Portfolio-Strukturen und Datenbank...
); } return (
{notice && (
{notice.message}
)} {/* Top Navbar */} {/* Main Admin Workspace Container */}
{/* Left Side: Navigation Links & Theme Context info */}

Aktive Identität

{profile.name}

{profile.name}

{activeProfile === "scifi" ? "Realname" : "Pseudonym"}
{/* Sub navigation inside the active profile */}

KI-Kopierassistent

Die 21. Jahrhundert Schreibzentrale. Generieren oder überarbeiten Sie Ihre Klappentexte für das Bücherregal mit modernster Gemini AI.

{/* Right Side: Active Workspace Editor Content */}
{error && (
{error}
)} {/* TAB 1: Profile Main Information Editor */} {activeTab === "profile" && (

Profil & Biografie verwalten

Bearbeiten Sie den öffentlichen Steckbrief für {profile.name}.

updateProfileField("name", e.target.value)} 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" />
updateProfileField("avatarUrl", url)} label="Autorenfoto / Avatar" />
updateProfileField("heroTitle", e.target.value)} 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" />
updateProfileField("heroSubtitle", e.target.value)} 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" />