2167 lines
116 KiB
TypeScript
2167 lines
116 KiB
TypeScript
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<AuthorData | null>(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<string | null>(null);
|
||
const [bookForm, setBookForm] = useState<Partial<Book>>({});
|
||
const [genreInputRaw, setGenreInputRaw] = useState("");
|
||
|
||
const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
|
||
const [projectForm, setProjectForm] = useState<Partial<Project>>({});
|
||
|
||
// Legal Documents state
|
||
const [editingLegalId, setEditingLegalId] = useState<string | null>(null);
|
||
const [legalForm, setLegalForm] = useState<Partial<LegalDocument>>({});
|
||
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 <AdminLogin password={password} error={error} loading={loading} onPasswordChange={setPassword} onSubmit={handleLogin} />;
|
||
}
|
||
|
||
if (!authorData || !profile) {
|
||
return (
|
||
<div className="min-h-screen bg-slate-950 text-slate-100 flex items-center justify-center text-sm font-mono">
|
||
<RefreshCw className="w-6 h-6 animate-spin text-cyan-400 mr-2" />
|
||
Lade Portfolio-Strukturen und Datenbank...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-950 text-slate-200">
|
||
{notice && (
|
||
<div
|
||
role="status"
|
||
aria-live="polite"
|
||
className={`fixed right-4 top-4 z-[70] max-w-sm rounded-xl border px-4 py-3 text-sm shadow-2xl ${notice.tone === "success" ? "border-emerald-700 bg-emerald-950 text-emerald-100" : "border-rose-800 bg-rose-950 text-rose-100"}`}
|
||
>
|
||
{notice.message}
|
||
</div>
|
||
)}
|
||
|
||
{/* Top Navbar */}
|
||
<nav className="bg-slate-900/80 border-b border-slate-800 sticky top-0 z-30 backdrop-blur-md">
|
||
<div className="max-w-7xl mx-auto px-4 py-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||
<div className="flex items-center gap-3">
|
||
<span className="p-2 bg-indigo-950 border border-indigo-700/50 rounded-lg text-cyan-400 font-mono text-sm">A</span>
|
||
<div>
|
||
<h1 className="text-lg font-bold text-white flex items-center gap-2">
|
||
<span>Autoren-Administration</span>
|
||
</h1>
|
||
<p className="text-[10px] text-slate-400 font-mono uppercase tracking-wider">Dual Identity System v1.5</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
{/* Swapping genre profile profile */}
|
||
<div className="bg-slate-950 p-0.5 rounded-xl border border-slate-800 flex flex-wrap gap-0.5">
|
||
<button
|
||
onClick={() => { setActiveProfile("scifi"); setActiveTab("profile"); }}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "scifi" ? "bg-cyan-500 text-slate-950" : "text-slate-400 hover:text-white"}`}
|
||
>
|
||
Sci-Fi ({authorData.scifi.name})
|
||
</button>
|
||
<button
|
||
onClick={() => { setActiveProfile("erotica"); setActiveTab("profile"); }}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "erotica" ? "bg-rose-800 text-white" : "text-slate-400 hover:text-white"}`}
|
||
>
|
||
Erotik ({authorData.erotica.name})
|
||
</button>
|
||
<button
|
||
onClick={() => { setActiveProfile("clara"); setActiveTab("profile"); }}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "clara" ? "bg-emerald-700 text-white" : "text-slate-400 hover:text-white"}`}
|
||
>
|
||
Naturlyrik ({authorData.clara.name})
|
||
</button>
|
||
<button
|
||
onClick={() => { setActiveProfile("renee"); setActiveTab("profile"); }}
|
||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "renee" ? "bg-yellow-500 text-slate-950" : "text-slate-400 hover:text-white"}`}
|
||
>
|
||
Romantik ({authorData.renee.name})
|
||
</button>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleLogoutLocal}
|
||
className="px-3 py-1.5 bg-slate-800 hover:bg-rose-950 border border-slate-700 hover:border-rose-900 rounded-lg text-xs font-mono text-slate-300 hover:text-white transition-all flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<LogOut className="w-3.5 h-3.5" />
|
||
<span>Abmelden</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
{/* Main Admin Workspace Container */}
|
||
<div className="max-w-7xl mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||
|
||
{/* Left Side: Navigation Links & Theme Context info */}
|
||
<div className="lg:col-span-3 space-y-4">
|
||
<div className="p-4 rounded-2xl bg-slate-900 border border-slate-800 space-y-4">
|
||
<h3 className="text-xs font-mono text-indigo-400 uppercase tracking-wider">Aktive Identität</h3>
|
||
<div className="flex items-center gap-3">
|
||
<img
|
||
src={profile.avatarUrl || undefined}
|
||
alt={profile.name}
|
||
className="w-10 h-10 rounded-full object-cover border border-slate-700"
|
||
referrerPolicy="no-referrer"
|
||
/>
|
||
<div>
|
||
<h4 className="text-sm font-bold text-white">{profile.name}</h4>
|
||
<span className="text-[10px] text-slate-500 uppercase tracking-widest">{activeProfile === "scifi" ? "Realname" : "Pseudonym"}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sub navigation inside the active profile */}
|
||
<div className="flex flex-col gap-1 pt-2">
|
||
<button
|
||
onClick={() => setActiveTab("profile")}
|
||
className={`px-3 py-2.5 rounded-xl text-left text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${activeTab === "profile" ? "bg-slate-800 border-l-4 border-indigo-500 text-white" : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||
>
|
||
<Compass className="w-4 h-4" />
|
||
<span>Profil & Biografie info</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("texts")}
|
||
className={`px-3 py-2.5 rounded-xl text-left text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${activeTab === "texts" ? "bg-slate-800 border-l-4 border-indigo-500 text-white" : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||
>
|
||
<Type className="w-4 h-4" />
|
||
<span>Texte & Überschriften</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("design")}
|
||
className={`px-3 py-2.5 rounded-xl text-left text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${activeTab === "design" ? "bg-slate-800 border-l-4 border-indigo-500 text-white" : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||
>
|
||
<Palette className="w-4 h-4" />
|
||
<span>Design & Farben</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("projects")}
|
||
className={`px-3 py-2.5 rounded-xl text-left text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${activeTab === "projects" ? "bg-slate-800 border-l-4 border-indigo-500 text-white" : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||
>
|
||
<Clipboard className="w-4 h-4" />
|
||
<span>Aktuelle Projekte ({profile.projects.length})</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab("books")}
|
||
className={`px-3 py-2.5 rounded-xl text-left text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${activeTab === "books" ? "bg-slate-800 border-l-4 border-indigo-500 text-white" : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||
>
|
||
<BookOpen className="w-4 h-4" />
|
||
<span>Bücherregal ({profile.books.length})</span>
|
||
</button>
|
||
<button
|
||
onClick={() => { setActiveTab("legal"); setEditingLegalId(null); setLegalForm({}); }}
|
||
className={`px-3 py-2.5 rounded-xl text-left text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${activeTab === "legal" ? "bg-slate-800 border-l-4 border-indigo-500 text-white" : "text-slate-400 hover:bg-slate-800/40 hover:text-white"}`}
|
||
>
|
||
<Scale className="w-4 h-4" />
|
||
<span>Rechtliches & Impressum ({(authorData.legalDocuments || []).length})</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 rounded-2xl bg-gradient-to-br from-indigo-950/20 to-slate-900 border border-slate-800/80 space-y-3">
|
||
<h4 className="text-xs font-mono text-cyan-400 uppercase tracking-wider flex items-center gap-1.5">
|
||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||
<span>KI-Kopierassistent</span>
|
||
</h4>
|
||
<p className="text-[11px] text-slate-300 leading-relaxed">
|
||
Die 21. Jahrhundert Schreibzentrale. Generieren oder überarbeiten Sie Ihre Klappentexte für das Bücherregal mit modernster Gemini AI.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Side: Active Workspace Editor Content */}
|
||
<div className="lg:col-span-9 space-y-6">
|
||
|
||
{error && (
|
||
<div className="p-3 bg-rose-950/40 border border-rose-900/50 text-rose-300 text-xs rounded-xl text-center">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 1: Profile Main Information Editor */}
|
||
{activeTab === "profile" && (
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-6">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||
<div>
|
||
<h2 className="text-lg font-bold text-white">Profil & Biografie verwalten</h2>
|
||
<p className="text-xs text-slate-400">Bearbeiten Sie den öffentlichen Steckbrief für {profile.name}.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => handleSaveProfileInfo(activeProfile)}
|
||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Save className="w-3.5 h-3.5" />
|
||
<span>Profil speichern</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Öffentlicher Name</label>
|
||
<input
|
||
type="text"
|
||
value={profile.name}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Avatar / Autorenfoto</label>
|
||
<ImagePicker
|
||
value={profile.avatarUrl}
|
||
onChange={(url) => updateProfileField("avatarUrl", url)}
|
||
label="Autorenfoto / Avatar"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Hero Titel (Hauptüberschrift)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.heroTitle}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Hero Untertitel</label>
|
||
<input
|
||
type="text"
|
||
value={profile.heroSubtitle}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Über Mich / Biografie</label>
|
||
<textarea
|
||
rows={8}
|
||
value={profile.bio}
|
||
onChange={(e) => updateProfileField("bio", 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 font-sans"
|
||
/>
|
||
</div>
|
||
|
||
{/* Dynamic Schlagworte / Highlights */}
|
||
<div className="md:col-span-2 border-t border-slate-800/80 pt-6 mt-2 space-y-4">
|
||
<div>
|
||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||
<Tag className="w-4 h-4 text-amber-400" />
|
||
<span>Schlagworte & Fokus (3 Infoboxen unter "Über mich")</span>
|
||
</h4>
|
||
<p className="text-xs text-slate-500">Diese drei Kacheln erscheinen unter der Biografie. Sie können die Kategoriename (z.B. GENRE, TONALITÄT, FOKUS) und den zugehörigen Inhalt frei anpassen.</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
{/* Schlagwort 1 */}
|
||
<div className="p-3 bg-slate-950 border border-slate-800 rounded-xl space-y-2">
|
||
<div className="text-xs font-semibold text-amber-400 uppercase flex items-center gap-1">
|
||
<span>Schlagwort 1</span>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Kategorie-Titel</label>
|
||
<input
|
||
type="text"
|
||
value={profile.bioTag1Label || ""}
|
||
onChange={(e) => updateProfileField("bioTag1Label", e.target.value)}
|
||
placeholder="z.B. GENRE"
|
||
className="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-white text-xs"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Inhalt / Wert</label>
|
||
<input
|
||
type="text"
|
||
value={profile.bioTag1Value || ""}
|
||
onChange={(e) => updateProfileField("bioTag1Value", e.target.value)}
|
||
placeholder="z.B. Emotionale Romance"
|
||
className="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-white text-xs font-medium"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Schlagwort 2 */}
|
||
<div className="p-3 bg-slate-950 border border-slate-800 rounded-xl space-y-2">
|
||
<div className="text-xs font-semibold text-amber-400 uppercase flex items-center gap-1">
|
||
<span>Schlagwort 2</span>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Kategorie-Titel</label>
|
||
<input
|
||
type="text"
|
||
value={profile.bioTag2Label || ""}
|
||
onChange={(e) => updateProfileField("bioTag2Label", e.target.value)}
|
||
placeholder="z.B. TONALITÄT"
|
||
className="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-white text-xs"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Inhalt / Wert</label>
|
||
<input
|
||
type="text"
|
||
value={profile.bioTag2Value || ""}
|
||
onChange={(e) => updateProfileField("bioTag2Value", e.target.value)}
|
||
placeholder="z.B. Herzlich & Hoffnungsvoll"
|
||
className="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-white text-xs font-medium"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Schlagwort 3 */}
|
||
<div className="p-3 bg-slate-950 border border-slate-800 rounded-xl space-y-2">
|
||
<div className="text-xs font-semibold text-amber-400 uppercase flex items-center gap-1">
|
||
<span>Schlagwort 3</span>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Kategorie-Titel</label>
|
||
<input
|
||
type="text"
|
||
value={profile.bioTag3Label || ""}
|
||
onChange={(e) => updateProfileField("bioTag3Label", e.target.value)}
|
||
placeholder="z.B. FOKUS"
|
||
className="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-white text-xs"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Inhalt / Wert</label>
|
||
<input
|
||
type="text"
|
||
value={profile.bioTag3Value || ""}
|
||
onChange={(e) => updateProfileField("bioTag3Value", e.target.value)}
|
||
placeholder="z.B. Gefühl & Tiefe"
|
||
className="w-full px-2.5 py-1.5 bg-slate-900 border border-slate-800 rounded-lg text-white text-xs font-medium"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Custom Domain & Routing Section */}
|
||
<div className="md:col-span-2 border-t border-slate-800/80 pt-6 mt-2 space-y-4">
|
||
<div>
|
||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||
<Globe className="w-4 h-4 text-indigo-400" />
|
||
<span>Domain- & Routing-Einstellungen</span>
|
||
</h4>
|
||
<p className="text-xs text-slate-500">Legen Sie fest, unter welchen Domains und Pfaden dieses Profil geladen werden soll. Sobald Besucher eine dieser Domains aufrufen, zeigt die Applikation automatisch diese Autoren-Suite an.</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Eigene Domains (Domain-Routing)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.customDomain || ""}
|
||
onChange={(e) => updateProfileField("customDomain", e.target.value)}
|
||
placeholder="z.B. autorin.de, www.autorin.de"
|
||
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"
|
||
/>
|
||
<p className="text-[11px] text-slate-500">Kommagetrennte Domainnamen. Bsp: <code className="text-slate-400">autorin.de, autorin.com</code></p>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Eigener Pfad-Prefix (Pfad-Routing)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.customPath || ""}
|
||
onChange={(e) => updateProfileField("customPath", e.target.value)}
|
||
placeholder="z.B. /renee-heart, /renee"
|
||
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"
|
||
/>
|
||
<p className="text-[11px] text-slate-500">Kommagetrennte Pfade für Direktlinks & Vorschau. Bsp: <code className="text-slate-400">/renee-heart, /renee</code></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Custom Highlight Section */}
|
||
<div className="md:col-span-2 border-t border-slate-800/80 pt-6 mt-2 space-y-4">
|
||
<div>
|
||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||
<span>Zusätzlicher Textabschnitt (unten auf der Seite)</span>
|
||
</h4>
|
||
<p className="text-xs text-slate-500">Ein frei gestaltbarer Infobereich, der am Ende des Portfolios (nach dem Bücherregal) dargestellt wird. Ideal für Termine, Lesungen, Newsletter-Aufrufe oder persönliche Grüße. Leer lassen, um den Bereich auszublenden.</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Abschnitts-Titel</label>
|
||
<input
|
||
type="text"
|
||
value={profile.customSectionTitle || ""}
|
||
onChange={(e) => updateProfileField("customSectionTitle", e.target.value)}
|
||
placeholder="z.B. Lesungen, Kontakt oder Newsletter"
|
||
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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Abschnitts-Inhalt (Markdown & Links unterstützt)</label>
|
||
<textarea
|
||
rows={4}
|
||
value={profile.customSectionContent || ""}
|
||
onChange={(e) => updateProfileField("customSectionContent", e.target.value)}
|
||
placeholder="Ihr individueller Textabschnitt..."
|
||
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-sans"
|
||
/>
|
||
</div>
|
||
<div className="space-y-3 md:col-span-2 pt-2">
|
||
<div>
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Optionale Buttons (maximal 3)</label>
|
||
<p className="text-[11px] text-slate-500 mt-1">Ein Button erscheint nur, wenn Beschriftung und eine vollständige HTTP(S)-Adresse eingetragen sind. Damit freigegebene Ziele werden öffentlich verlinkt.</p>
|
||
</div>
|
||
{[0, 1, 2].map((index) => (
|
||
<div key={index} className="grid grid-cols-1 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] gap-3 rounded-xl border border-slate-800 bg-slate-950/50 p-3">
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-mono text-slate-500 uppercase">Button {index + 1} – Beschriftung</label>
|
||
<input
|
||
type="text"
|
||
maxLength={80}
|
||
value={profile.customSectionLinks?.[index]?.label || ""}
|
||
onChange={(event) => updateCustomSectionLink(index, "label", event.target.value)}
|
||
placeholder="z.B. Zur anderen Autorin"
|
||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-lg text-white outline-none focus:border-indigo-500 text-sm"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-mono text-slate-500 uppercase">Ziel-URL</label>
|
||
<input
|
||
type="url"
|
||
value={profile.customSectionLinks?.[index]?.url || ""}
|
||
onChange={(event) => updateCustomSectionLink(index, "url", event.target.value)}
|
||
placeholder="https://www.beispiel.de"
|
||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-lg text-white outline-none focus:border-indigo-500 text-sm font-mono"
|
||
/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 2: Static Texts & Headings */}
|
||
{activeTab === "texts" && (
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-6">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||
<div>
|
||
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||
<Type className="w-5 h-5 text-indigo-400" />
|
||
<span>Statische Texte & Sektionsüberschriften</span>
|
||
</h2>
|
||
<p className="text-xs text-slate-400">Passen Sie alle Beschriftungen, Sektions-Header und Fußzeilen für {profile.name} an.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => handleSaveProfileInfo(activeProfile)}
|
||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Save className="w-3.5 h-3.5" />
|
||
<span>Texte speichern</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Header Badge-Text (kleiner Tag oben)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.badgeText || ""}
|
||
onChange={(e) => updateProfileField("badgeText", e.target.value)}
|
||
placeholder="z.B. Cyber-Mystisches Archive"
|
||
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"
|
||
/>
|
||
<p className="text-[10px] text-slate-500">Wird über dem Haupttitel mit feinem Glanz-Effekt angezeigt.</p>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase font-bold">"Über mich" Sektionsüberschrift</label>
|
||
<input
|
||
type="text"
|
||
value={profile.aboutTitle || ""}
|
||
onChange={(e) => updateProfileField("aboutTitle", e.target.value)}
|
||
placeholder="z.B. Über den Autor"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-amber-400 flex items-center gap-1">
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
<span>Spotlight / Neuerscheinung Sektionsüberschrift</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={profile.spotlightTitle || ""}
|
||
onChange={(e) => updateProfileField("spotlightTitle", e.target.value)}
|
||
placeholder="z.B. Spotlight & Neuerscheinung"
|
||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-amber-500 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase text-amber-400">Spotlight Untertitel / Beschreibung</label>
|
||
<input
|
||
type="text"
|
||
value={profile.spotlightSubtitle || ""}
|
||
onChange={(e) => updateProfileField("spotlightSubtitle", e.target.value)}
|
||
placeholder="z.B. Besonders hervorgehobenes Werk im Fokus"
|
||
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-amber-500 text-sm"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Projekte Sektionsüberschrift</label>
|
||
<input
|
||
type="text"
|
||
value={profile.projectsTitle || ""}
|
||
onChange={(e) => updateProfileField("projectsTitle", e.target.value)}
|
||
placeholder="z.B. Aktuelle Projekte"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Projekte Untertitel / Beschreibung</label>
|
||
<input
|
||
type="text"
|
||
value={profile.projectsSubtitle || ""}
|
||
onChange={(e) => updateProfileField("projectsSubtitle", e.target.value)}
|
||
placeholder="z.B. Ein exklusiver Blick auf mein Schreib-Amboss..."
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Projekt-Detail Exzerpt-Überschrift (im Modal)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.projectExcerptTitle || ""}
|
||
onChange={(e) => updateProfileField("projectExcerptTitle", e.target.value)}
|
||
placeholder="z.B. Projekt-Exzerpt & Fortschrittsnotizen"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Projekt-Detail Badge / Wasserzeichen (z.B. SEC_REPORT_LOG)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.projectExcerptBadge || ""}
|
||
onChange={(e) => updateProfileField("projectExcerptBadge", e.target.value)}
|
||
placeholder="z.B. SEC_REPORT_LOG, FRAGMENT_LOG_01..."
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Projekt-Fortschritts-Label (z.B. Organischer Fortschritt)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.projectProgressLabel || ""}
|
||
onChange={(e) => updateProfileField("projectProgressLabel", e.target.value)}
|
||
placeholder="z.B. Organischer Fortschritt, Reifegrad..."
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Bücherregal Sektionsüberschrift</label>
|
||
<input
|
||
type="text"
|
||
value={profile.booksTitle || ""}
|
||
onChange={(e) => updateProfileField("booksTitle", e.target.value)}
|
||
placeholder="z.B. Bücherregal"
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Bücherregal Untertitel / Beschreibung</label>
|
||
<input
|
||
type="text"
|
||
value={profile.booksSubtitle || ""}
|
||
onChange={(e) => updateProfileField("booksSubtitle", e.target.value)}
|
||
placeholder="z.B. Entdecken Sie meine verlegten Werke..."
|
||
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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Footer-Text (Fußzeile ganz unten)</label>
|
||
<input
|
||
type="text"
|
||
value={profile.footerText || ""}
|
||
onChange={(e) => updateProfileField("footerText", e.target.value)}
|
||
placeholder="z.B. Alle Rechte vorbehalten..."
|
||
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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 3: Design & Dynamic Theme Controls */}
|
||
{activeTab === "design" && (
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-6">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||
<div>
|
||
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||
<Palette className="w-5 h-5 text-indigo-400" />
|
||
<span>Design, Farben & Themes</span>
|
||
</h2>
|
||
<p className="text-xs text-slate-400">Gestalten Sie das Erscheinungsbild für {profile.name} nach Ihren Wünschen.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => handleSaveProfileInfo(activeProfile)}
|
||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Save className="w-3.5 h-3.5" />
|
||
<span>Design speichern</span>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Preset Theme Selection */}
|
||
<div className="space-y-3">
|
||
<label className="text-xs font-mono text-cyan-400 uppercase tracking-widest font-bold">Schnell-Paletten (Presets)</label>
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
updateProfileField("accentColor", "#06b6d4");
|
||
updateProfileField("secondaryColor", "#a855f7");
|
||
updateProfileField("backgroundColor", "#020617");
|
||
updateProfileField("cardBgColor", "#0f172a");
|
||
updateProfileField("fontFamily", "sans");
|
||
}}
|
||
className="p-3 bg-slate-950 border border-cyan-500/40 hover:border-cyan-400 rounded-xl text-left space-y-2 cursor-pointer transition-all"
|
||
>
|
||
<div className="flex gap-1.5">
|
||
<div className="w-4 h-4 rounded-full bg-[#06b6d4]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#a855f7]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#020617] border border-slate-700" />
|
||
</div>
|
||
<div className="text-xs font-bold text-white">Neon Sci-Fi</div>
|
||
<div className="text-[10px] text-slate-400">Cyan & Purple Slate</div>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
updateProfileField("accentColor", "#f97316");
|
||
updateProfileField("secondaryColor", "#f59e0b");
|
||
updateProfileField("backgroundColor", "#0a0a0a");
|
||
updateProfileField("cardBgColor", "#171717");
|
||
updateProfileField("fontFamily", "serif");
|
||
}}
|
||
className="p-3 bg-slate-950 border border-orange-500/40 hover:border-orange-400 rounded-xl text-left space-y-2 cursor-pointer transition-all"
|
||
>
|
||
<div className="flex gap-1.5">
|
||
<div className="w-4 h-4 rounded-full bg-[#f97316]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#f59e0b]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#0a0a0a] border border-slate-700" />
|
||
</div>
|
||
<div className="text-xs font-bold text-white">Samt & Peach</div>
|
||
<div className="text-[10px] text-slate-400">Orange & Amber Dark</div>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
updateProfileField("accentColor", "#10b981");
|
||
updateProfileField("secondaryColor", "#84cc16");
|
||
updateProfileField("backgroundColor", "#064e3b");
|
||
updateProfileField("cardBgColor", "#022c22");
|
||
updateProfileField("fontFamily", "serif");
|
||
}}
|
||
className="p-3 bg-slate-950 border border-emerald-500/40 hover:border-emerald-400 rounded-xl text-left space-y-2 cursor-pointer transition-all"
|
||
>
|
||
<div className="flex gap-1.5">
|
||
<div className="w-4 h-4 rounded-full bg-[#10b981]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#84cc16]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#064e3b] border border-slate-700" />
|
||
</div>
|
||
<div className="text-xs font-bold text-white">Wald & Salbei</div>
|
||
<div className="text-[10px] text-slate-400">Emerald & Lime Forest</div>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
updateProfileField("accentColor", "#ec4899");
|
||
updateProfileField("secondaryColor", "#8b5cf6");
|
||
updateProfileField("backgroundColor", "#09090b");
|
||
updateProfileField("cardBgColor", "#18181b");
|
||
updateProfileField("fontFamily", "sans");
|
||
}}
|
||
className="p-3 bg-slate-950 border border-pink-500/40 hover:border-pink-400 rounded-xl text-left space-y-2 cursor-pointer transition-all"
|
||
>
|
||
<div className="flex gap-1.5">
|
||
<div className="w-4 h-4 rounded-full bg-[#ec4899]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#8b5cf6]" />
|
||
<div className="w-4 h-4 rounded-full bg-[#09090b] border border-slate-700" />
|
||
</div>
|
||
<div className="text-xs font-bold text-white">Obsidian Magenta</div>
|
||
<div className="text-[10px] text-slate-400">Pink & Violet Dark</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-2">
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Haupt-Akzentfarbe</label>
|
||
<div className="flex gap-2 items-center">
|
||
<input
|
||
type="color"
|
||
value={profile.accentColor || "#06b6d4"}
|
||
onChange={(e) => updateProfileField("accentColor", e.target.value)}
|
||
className="w-10 h-10 rounded-lg cursor-pointer bg-slate-950 border border-slate-800 p-1"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={profile.accentColor || "#06b6d4"}
|
||
onChange={(e) => updateProfileField("accentColor", e.target.value)}
|
||
className="flex-1 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Sekundär-Farbe</label>
|
||
<div className="flex gap-2 items-center">
|
||
<input
|
||
type="color"
|
||
value={profile.secondaryColor || "#a855f7"}
|
||
onChange={(e) => updateProfileField("secondaryColor", e.target.value)}
|
||
className="w-10 h-10 rounded-lg cursor-pointer bg-slate-950 border border-slate-800 p-1"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={profile.secondaryColor || "#a855f7"}
|
||
onChange={(e) => updateProfileField("secondaryColor", e.target.value)}
|
||
className="flex-1 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Hintergrundfarbe (Seiten-Canvas)</label>
|
||
<div className="flex gap-2 items-center">
|
||
<input
|
||
type="color"
|
||
value={profile.backgroundColor || "#020617"}
|
||
onChange={(e) => updateProfileField("backgroundColor", e.target.value)}
|
||
className="w-10 h-10 rounded-lg cursor-pointer bg-slate-950 border border-slate-800 p-1"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={profile.backgroundColor || "#020617"}
|
||
onChange={(e) => updateProfileField("backgroundColor", e.target.value)}
|
||
className="flex-1 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Karten-Hintergrundfarbe</label>
|
||
<div className="flex gap-2 items-center">
|
||
<input
|
||
type="color"
|
||
value={profile.cardBgColor || "#0f172a"}
|
||
onChange={(e) => updateProfileField("cardBgColor", e.target.value)}
|
||
className="w-10 h-10 rounded-lg cursor-pointer bg-slate-950 border border-slate-800 p-1"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={profile.cardBgColor || "#0f172a"}
|
||
onChange={(e) => updateProfileField("cardBgColor", e.target.value)}
|
||
className="flex-1 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Schriftart / Typografie</label>
|
||
<select
|
||
value={profile.fontFamily || "sans"}
|
||
onChange={(e) => updateProfileField("fontFamily", e.target.value as any)}
|
||
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"
|
||
>
|
||
<option value="sans">Moderne Sans-Serif (Standard)</option>
|
||
<option value="serif">Klassische Serif (Elegante Buchschrift)</option>
|
||
<option value="mono">Futuristische Monospace (Code/Tech)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="space-y-2 md:col-span-2 pt-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white flex items-center gap-2">
|
||
<ImageIcon className="w-4 h-4 text-cyan-400" />
|
||
<span>Hero-Hintergrundbild (optional)</span>
|
||
</label>
|
||
<ImagePicker
|
||
value={profile.heroBannerUrl || ""}
|
||
onChange={(url) => updateProfileField("heroBannerUrl", url)}
|
||
label="Hero Banner-Bild"
|
||
/>
|
||
<p className="text-[10px] text-slate-500">Wenn angegeben, wird dieses Bild als stimmungsvoller Hintergrund im oberen Bereich der Webseite hinterlegt.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 2: Projects manager */}
|
||
{activeTab === "projects" && (
|
||
<div className="space-y-6">
|
||
{editingProjectId === null ? (
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||
<div>
|
||
<h2 className="text-lg font-bold text-white">Aktuelle Projekte</h2>
|
||
<p className="text-xs text-slate-400">Verwalten Sie Ihre laufenden Schreibarbeiten und zeigen Sie Ihren Fortschritt.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setEditingProjectId("new");
|
||
setProjectForm({ title: "", status: "Konzept", progress: 0, description: "", imageUrl: "", spotifyPlaylistId: "" });
|
||
}}
|
||
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>Neues Projekt anlegen</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{profile.projects.length === 0 ? (
|
||
<p className="text-xs text-slate-500 text-center py-6 font-mono">Keine laufenden Projekte eingetragen.</p>
|
||
) : (
|
||
profile.projects.map((proj) => (
|
||
<div key={proj.id} className="p-4 bg-slate-950 border border-slate-800 rounded-xl flex items-center justify-between gap-4">
|
||
<div className="space-y-1 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-[10px] font-mono text-indigo-400 bg-indigo-950 px-2 py-0.5 rounded uppercase border border-indigo-900/30">
|
||
{proj.status}
|
||
</span>
|
||
<h4 className="text-sm font-bold text-white">{proj.title}</h4>
|
||
</div>
|
||
<p className="text-xs text-slate-400 line-clamp-1">{proj.description}</p>
|
||
<div className="flex items-center gap-2 w-full max-w-[200px] pt-1">
|
||
<div className="h-1 bg-slate-800 rounded-full w-full overflow-hidden">
|
||
<div className="h-full bg-cyan-400 rounded-full" style={{ width: `${proj.progress}%` }} />
|
||
</div>
|
||
<span className="text-[10px] font-mono text-slate-550 text-slate-400">{proj.progress}%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => {
|
||
setEditingProjectId(proj.id);
|
||
setProjectForm(proj);
|
||
}}
|
||
className="p-1.5 hover:bg-slate-800 rounded-lg text-slate-400 hover:text-white transition-all cursor-pointer"
|
||
title="Bearbeiten"
|
||
>
|
||
<Edit2 className="w-4 h-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteProject(proj.id)}
|
||
className="p-1.5 hover:bg-rose-950 rounded-lg text-slate-400 hover:text-rose-450 text-rose-400 transition-all cursor-pointer"
|
||
title="Löschen"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Edit/Create Project Form */
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-4">
|
||
<h3 className="text-base font-bold text-white">
|
||
{editingProjectId === "new" ? "Neues Schreibprojekt erschaffen" : "Schreibprojekt editieren"}
|
||
</h3>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Projekttitel</label>
|
||
<input
|
||
type="text"
|
||
value={projectForm.title || ""}
|
||
onChange={(e) => setProjectForm({ ...projectForm, title: 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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Entwicklungsstatus</label>
|
||
<input
|
||
type="text"
|
||
value={projectForm.status || ""}
|
||
onChange={(e) => setProjectForm({ ...projectForm, status: e.target.value })}
|
||
placeholder="z.B. Schreibphase, Lektorat, Konzept"
|
||
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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-200 uppercase flex justify-between">
|
||
<span>Schreibfortschritt</span>
|
||
<span className="text-indigo-400 font-bold">{projectForm.progress || 0}%</span>
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="100"
|
||
value={projectForm.progress || 0}
|
||
onChange={(e) => setProjectForm({ ...projectForm, progress: parseInt(e.target.value) })}
|
||
className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-cyan-400"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Kurzbeschreibung / Vorschau (Markdown & Links unterstützt)</label>
|
||
<textarea
|
||
rows={3}
|
||
value={projectForm.description || ""}
|
||
onChange={(e) => setProjectForm({ ...projectForm, description: 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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Detailbild</label>
|
||
<ImagePicker
|
||
value={projectForm.imageUrl || ""}
|
||
onChange={(url) => setProjectForm({ ...projectForm, imageUrl: url })}
|
||
label="Projekt-Detailbild"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase flex items-center gap-1">
|
||
<Music className="w-3.5 h-3.5" />
|
||
<span>Spotify Playlist ID</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={projectForm.spotifyPlaylistId || ""}
|
||
onChange={(e) => setProjectForm({ ...projectForm, spotifyPlaylistId: e.target.value })}
|
||
placeholder="z.B. 47R6ZAdk7Xf6M4i9mR0fP1"
|
||
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"
|
||
/>
|
||
<span className="text-[9px] text-slate-500">Playlist-ID oder vollständige Spotify-Playlist-URL.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 pt-2">
|
||
<button
|
||
onClick={handleSaveProject}
|
||
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-bold flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
<span>Projekt sichern</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setEditingProjectId(null)}
|
||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-mono cursor-pointer"
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB 3: Books shelf manager */}
|
||
{activeTab === "books" && (
|
||
<div className="space-y-6">
|
||
{editingBookId === null ? (
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||
<div>
|
||
<h2 className="text-lg font-bold text-white">Bücherregal verwalten</h2>
|
||
<p className="text-xs text-slate-400">Fügen Sie Bücher hinzu oder bearbeiten Sie Beschreibungen und Spotify-Playlists.</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setEditingBookId("new");
|
||
setBookForm({
|
||
title: "", subtitle: "", coverUrl: "", description: "",
|
||
publishedYear: new Date().getFullYear(), buyLink: "",
|
||
spotifyPlaylistId: "", genres: [], isSpotlight: false, spotlightBadge: ""
|
||
});
|
||
setGenreInputRaw("");
|
||
// Preset Assistant blurb writer values to blank
|
||
setAiTitle("");
|
||
setAiIdeas("");
|
||
setAiResult("");
|
||
}}
|
||
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>Neues Buch hinzufügen</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||
{profile.books.length === 0 ? (
|
||
<p className="text-xs text-slate-500 text-center py-6 font-mono col-span-3">Keine Bücher im virtuellen Regal.</p>
|
||
) : (
|
||
profile.books.map((b) => (
|
||
<div key={b.id} className="p-4 bg-slate-950 border border-slate-800 rounded-xl space-y-3 flex flex-col justify-between">
|
||
<div className="space-y-3">
|
||
<div className="w-24 aspect-[2/3] mx-auto rounded-lg overflow-hidden border border-slate-800 bg-slate-900 relative">
|
||
<img src={b.coverUrl || undefined} alt={b.title} className="w-full h-full object-cover" referrerPolicy="no-referrer" />
|
||
{b.isSpotlight && (
|
||
<span className="absolute top-1 right-1 bg-amber-500 text-slate-950 p-1 rounded-full shadow-md" title="Spotlight / Neuerscheinung">
|
||
<Sparkles className="w-3 h-3" />
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="text-center space-y-0.5">
|
||
<h4 className="text-sm font-bold text-white line-clamp-1">{b.title}</h4>
|
||
<p className="text-[10px] text-indigo-400 font-mono italic uppercase">{b.subtitle || "Einzelband"}</p>
|
||
<span className="text-[10px] text-slate-500 font-mono">Datum/Jahr: {b.publishedYear}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center justify-center gap-2 pt-2 border-t border-slate-900">
|
||
<button
|
||
onClick={() => {
|
||
setEditingBookId(b.id);
|
||
setBookForm(b);
|
||
setGenreInputRaw(b.genres ? b.genres.join(", ") : "");
|
||
setAiTitle(b.title || "");
|
||
setAiResult("");
|
||
}}
|
||
className="px-3 py-1.5 bg-slate-900 hover:bg-slate-800 border border-slate-800 hover:border-slate-700 rounded-lg text-xs font-mono text-slate-300 flex items-center gap-1 cursor-pointer"
|
||
>
|
||
<Edit2 className="w-3.5 h-3.5" />
|
||
<span>Editieren</span>
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteBook(b.id)}
|
||
className="p-1.5 hover:bg-rose-950 text-rose-400 rounded-lg transition-all border border-transparent hover:border-rose-900 cursor-pointer"
|
||
title="Löschen"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Edit/Create Book Form with Integrated AI Synopsis generator */
|
||
<div className="space-y-6">
|
||
<div className="p-6 bg-slate-900 border border-slate-800 rounded-2xl space-y-4">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||
<h3 className="text-base font-bold text-white">
|
||
{editingBookId === "new" ? "Neues Buch ins Regal stellen" : "Buchinformationen editieren"}
|
||
</h3>
|
||
<span className="text-[10px] text-slate-500 font-mono">ID: {editingBookId}</span>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Spotlight Section Toggle inside Book Form */}
|
||
<div className="md:col-span-2 p-4 bg-amber-950/30 border border-amber-500/40 rounded-xl space-y-3">
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="checkbox"
|
||
id="isSpotlightCheckbox"
|
||
checked={!!bookForm.isSpotlight}
|
||
onChange={(e) => setBookForm({ ...bookForm, isSpotlight: e.target.checked })}
|
||
className="w-4 h-4 rounded accent-amber-500 cursor-pointer"
|
||
/>
|
||
<label htmlFor="isSpotlightCheckbox" className="text-sm font-bold text-amber-300 cursor-pointer flex items-center gap-2">
|
||
<Sparkles className="w-4 h-4 text-amber-400" />
|
||
<span>Als Neuerscheinung / Buch-Spotlight auf der Homepage hervorheben</span>
|
||
</label>
|
||
</div>
|
||
<p className="text-xs text-slate-400 pl-7">
|
||
Wenn aktiviert, wird dieses Werk in einer prominenten Highlight-Kachel zwischen "Über mich" und "Aktuelle Projekte" platziert.
|
||
</p>
|
||
|
||
{bookForm.isSpotlight && (
|
||
<div className="pl-7 pt-1 space-y-1">
|
||
<label className="text-[10px] font-mono text-amber-400 uppercase font-semibold">Individueller Badge-Text (optional)</label>
|
||
<input
|
||
type="text"
|
||
value={bookForm.spotlightBadge || ""}
|
||
onChange={(e) => setBookForm({ ...bookForm, spotlightBadge: e.target.value })}
|
||
placeholder="z.B. Neuerscheinung, Vorbestellbar, Herbst-Highlight..."
|
||
className="w-full px-3 py-1.5 bg-slate-950 border border-slate-800 rounded-lg text-white outline-none focus:border-amber-500 text-xs"
|
||
/>
|
||
<p className="text-[10px] text-slate-500">
|
||
Wenn leer, wird automatisch "Erscheint am [Datum]" (bei Zukunftsdatum) oder "Neuerscheinung" verwendet.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Buchtitel</label>
|
||
<input
|
||
type="text"
|
||
value={bookForm.title || ""}
|
||
onChange={(e) => {
|
||
setBookForm({ ...bookForm, title: e.target.value });
|
||
setAiTitle(e.target.value);
|
||
}}
|
||
placeholder="z.B. Caelum: Ruf der Sterne"
|
||
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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Untertitel / Reihe</label>
|
||
<input
|
||
type="text"
|
||
value={bookForm.subtitle || ""}
|
||
onChange={(e) => setBookForm({ ...bookForm, subtitle: e.target.value })}
|
||
placeholder="z.B. Arche-Chroniken I"
|
||
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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-white">Cover-Bild</label>
|
||
<ImagePicker
|
||
value={bookForm.coverUrl || ""}
|
||
onChange={(url) => setBookForm({ ...bookForm, coverUrl: url })}
|
||
label="Buch-Cover"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Veröffentlichungsdatum / -jahr</label>
|
||
<input
|
||
type="text"
|
||
value={bookForm.publishedYear || ""}
|
||
onChange={(e) => setBookForm({ ...bookForm, publishedYear: e.target.value })}
|
||
placeholder="z.B. 15.10.2026 oder 2025"
|
||
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"
|
||
/>
|
||
<p className="text-[10px] text-slate-500">Unterstützt Jahreszahlen (2026) und konkrete Daten (15.10.2026).</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase flex items-center gap-1">
|
||
<Eye className="w-3.5 h-3.5" />
|
||
<span>Buy-Link / Shop-Link (z.B. Amazon, Thalia)</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={bookForm.buyLink || ""}
|
||
onChange={(e) => setBookForm({ ...bookForm, buyLink: e.target.value })}
|
||
placeholder="https://..."
|
||
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"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase flex items-center gap-1">
|
||
<Music className="w-3.5 h-3.5" />
|
||
<span>Spotify Playlist ID (Webplayer Embed)</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={bookForm.spotifyPlaylistId || ""}
|
||
onChange={(e) => setBookForm({ ...bookForm, spotifyPlaylistId: e.target.value })}
|
||
placeholder="z.B. 47R6ZAdk7Xf6M4i9mR0fP1"
|
||
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"
|
||
/>
|
||
<span className="text-[9px] text-slate-500">Nur den ID-String eintragen, nicht die komplette URL.</span>
|
||
</div>
|
||
<div className="space-y-1 md:col-span-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Genres (Kommagetrennt)</label>
|
||
<input
|
||
type="text"
|
||
value={genreInputRaw}
|
||
onChange={(e) => setGenreInputRaw(e.target.value)}
|
||
placeholder="z.B. Science-Fiction, Cyberpunk, Drama"
|
||
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"
|
||
/>
|
||
<p className="text-[10px] text-slate-500">Trennen Sie mehrere Genres einfach durch Kommas.</p>
|
||
</div>
|
||
|
||
{/* Buch-Klappentext Synopsis / Description */}
|
||
<div className="space-y-1 md:col-span-2">
|
||
<div className="flex items-center justify-between">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Buch-Klappentext (Synopsis)</label>
|
||
<span className="text-[10px] text-purple-400 font-mono">Markdown & Absätze erlaubt</span>
|
||
</div>
|
||
<textarea
|
||
rows={6}
|
||
value={bookForm.description || ""}
|
||
onChange={(e) => setBookForm({ ...bookForm, description: 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 font-sans"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-3 pt-4 border-t border-slate-800">
|
||
<button
|
||
onClick={handleSaveBook}
|
||
className="px-4 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg text-xs font-bold flex items-center gap-1.5 cursor-pointer"
|
||
>
|
||
<Check className="w-4 h-4" />
|
||
<span>Sichern & ins Regal stellen</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setEditingBookId(null)}
|
||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-xs font-mono cursor-pointer"
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 21st Century KI Integration - Gemini 3.5 Blurb generator tool panel */}
|
||
<div className="p-6 bg-gradient-to-r from-slate-900 to-indigo-950 border border-indigo-900/50 rounded-2xl space-y-4">
|
||
<div className="flex items-center justify-between border-b border-indigo-900/30 pb-3">
|
||
<h4 className="text-sm font-bold text-white flex items-center gap-2">
|
||
<Sparkles className="w-4 h-4 text-cyan-400" />
|
||
<span>KI-Schreibwerkstatt: Klappentext-Generator</span>
|
||
</h4>
|
||
<span className="text-[10px] text-cyan-400 font-mono tracking-wide">Powered by Gemini 3.5</span>
|
||
</div>
|
||
|
||
<div className="space-y-3 text-xs leading-relaxed text-slate-300">
|
||
<p>
|
||
Haben Sie eine Schreibblockade? Geben Sie ein paar Stichworte Ihrer Ideen ein. Unser KI-Assistent formuliert für Sie einen verkaufsfördernden und atmosphärischen Klappentext passend zur Identität!
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Assistenten-Buchtitel</label>
|
||
<input
|
||
type="text"
|
||
value={aiTitle}
|
||
onChange={(e) => setAiTitle(e.target.value)}
|
||
className="w-full px-3 py-1.5 bg-slate-950 border border-slate-800 rounded-lg text-white text-xs outline-none focus:border-indigo-500"
|
||
placeholder="Aktueller Titel"
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Genre / Stilvorgabe</label>
|
||
<input
|
||
type="text"
|
||
value={aiGenre}
|
||
onChange={(e) => setAiGenre(e.target.value)}
|
||
className="w-full px-3 py-1.5 bg-slate-950 border border-slate-800 rounded-lg text-white text-xs outline-none focus:border-indigo-500"
|
||
placeholder={activeProfile === "scifi" ? "z.B. Space-Opera, Runenmagie" : "z.B. Elegante Romanze, Verbotenes Begehren"}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Tonalität / Stimmung</label>
|
||
<select
|
||
value={aiTone}
|
||
onChange={(e) => setAiTone(e.target.value)}
|
||
className="w-full px-3 py-1.5 bg-slate-950 border border-slate-800 rounded-lg text-white text-xs outline-none focus:border-indigo-500"
|
||
>
|
||
<option value="Geheimnisvoll & Episch">Geheimnisvoll & Episch</option>
|
||
<option value="Dystopisch & Düster">Dystopisch & Düster</option>
|
||
<option value="Sinnlich & Knisternd">Sinnlich & Knisternd (Erotisch)</option>
|
||
<option value="Emotional & Melancholisch">Emotional & Melancholisch</option>
|
||
<option value="Actionreich & Fesselnd">Actionreich & Fesselnd</option>
|
||
</select>
|
||
</div>
|
||
<div className="space-y-1 md:col-span-3">
|
||
<label className="text-[10px] font-mono text-slate-400 uppercase">Stichpunkte, Motive, Kontexte (Ideen)</label>
|
||
<textarea
|
||
rows={3}
|
||
value={aiIdeas}
|
||
onChange={(e) => setAiIdeas(e.target.value)}
|
||
placeholder="z.B. Raumschiffabsturz, uralter Fluch auf einer Wüstenwelt, Geheimidentität und verbotene Leidenschaft..."
|
||
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-xs"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex justify-between items-center pt-2">
|
||
<button
|
||
onClick={handleGenerateAISynopsis}
|
||
disabled={aiLoading}
|
||
className="px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white rounded-lg text-xs font-bold flex items-center gap-1.5 cursor-pointer disabled:opacity-50"
|
||
>
|
||
{aiLoading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Sparkles className="w-3.5 h-3.5 text-cyan-300" />}
|
||
<span>Synopsis generieren und ins Formular eintragen</span>
|
||
</button>
|
||
</div>
|
||
|
||
{aiResult && (
|
||
<div className="p-4 bg-slate-950/80 border border-indigo-950 rounded-xl space-y-2 text-xs font-sans">
|
||
<div className="text-[10px] font-mono text-cyan-400 uppercase">Generierter Entwurf:</div>
|
||
<p className="text-slate-350 leading-relaxed italic whitespace-pre-wrap">{aiResult}</p>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* TAB: LEGAL DOCUMENTS (IMPRESSUM & DATENSCHUTZ) */}
|
||
{activeTab === "legal" && (
|
||
<div className="p-6 md:p-8 space-y-8">
|
||
|
||
{/* Header */}
|
||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 border-b border-slate-800 pb-6">
|
||
<div>
|
||
<h3 className="text-xl font-bold text-white flex items-center gap-2">
|
||
<Scale className="w-5 h-5 text-indigo-400" />
|
||
<span>Rechtliche Pflichtangaben & Dokumente</span>
|
||
</h3>
|
||
<p className="text-xs text-slate-400 mt-1">
|
||
Erstellen und verwalten Sie Impressum, Datenschutzerklärungen und individuelle Rechtsbelehrungen für Ihre Autorenprofile.
|
||
</p>
|
||
</div>
|
||
|
||
{!editingLegalId && (
|
||
<button
|
||
onClick={() => {
|
||
setEditingLegalId("new");
|
||
setLegalForm({
|
||
title: "Impressum",
|
||
type: "impressum",
|
||
updatedAt: new Date().toISOString().split("T")[0],
|
||
assignedProfiles: ["scifi", "erotica", "clara", "renee"],
|
||
content: ""
|
||
});
|
||
setLegalPreviewMode("edit");
|
||
}}
|
||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer shadow-lg shadow-indigo-950/50"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>Neues Dokument anlegen</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Document List when not editing */}
|
||
{!editingLegalId ? (
|
||
<div className="space-y-6">
|
||
{(!authorData.legalDocuments || authorData.legalDocuments.length === 0) ? (
|
||
<div className="p-12 text-center bg-slate-900/40 border border-slate-800 border-dashed rounded-2xl space-y-4">
|
||
<Scale className="w-10 h-10 text-slate-600 mx-auto" />
|
||
<div>
|
||
<h4 className="text-sm font-bold text-white">Noch keine rechtlichen Dokumente angelegt</h4>
|
||
<p className="text-xs text-slate-400 max-w-md mx-auto mt-1">
|
||
Legen Sie ein Impressum oder eine Datenschutzerklärung an, um rechtliche Transparenz auf Ihren Portfolioseiten zu gewährleisten.
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={() => {
|
||
setEditingLegalId("new");
|
||
setLegalForm({
|
||
title: "Impressum",
|
||
type: "impressum",
|
||
updatedAt: new Date().toISOString().split("T")[0],
|
||
assignedProfiles: ["scifi", "erotica", "clara", "renee"],
|
||
content: ""
|
||
});
|
||
}}
|
||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-bold transition-all inline-flex items-center gap-2 cursor-pointer"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
<span>Erstes Dokument anlegen</span>
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 gap-4">
|
||
{authorData.legalDocuments.map((doc) => {
|
||
const profileNamesMap = {
|
||
scifi: authorData.scifi.name,
|
||
erotica: authorData.erotica.name,
|
||
clara: authorData.clara.name,
|
||
renee: authorData.renee.name
|
||
};
|
||
|
||
return (
|
||
<div
|
||
key={doc.id}
|
||
className="p-5 bg-slate-900 border border-slate-800 hover:border-slate-700 rounded-2xl transition-all space-y-4 shadow-sm"
|
||
>
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||
<div className="flex items-center gap-3">
|
||
<div className="p-2.5 bg-slate-950 border border-slate-800 rounded-xl text-indigo-400">
|
||
{doc.type === "impressum" ? (
|
||
<Scale className="w-5 h-5" />
|
||
) : doc.type === "privacy" ? (
|
||
<ShieldCheck className="w-5 h-5 text-emerald-400" />
|
||
) : (
|
||
<FileText className="w-5 h-5 text-cyan-400" />
|
||
)}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<h4 className="text-base font-bold text-white">{doc.title}</h4>
|
||
<span className="px-2 py-0.5 bg-slate-800 border border-slate-700 text-slate-300 text-[10px] font-mono rounded-md">
|
||
{doc.type === "impressum" ? "Impressum (§ 5 DDG)" : doc.type === "privacy" ? "Datenschutz (DSGVO)" : "Individuell"}
|
||
</span>
|
||
</div>
|
||
<span className="text-[11px] text-slate-500 font-mono">
|
||
Stand: {doc.updatedAt || "Aktuell"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => handleDuplicateLegalDocument(doc)}
|
||
title="Dokument duplizieren"
|
||
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white rounded-lg text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1.5"
|
||
>
|
||
<Copy className="w-3.5 h-3.5" />
|
||
<span className="hidden sm:inline">Kopieren</span>
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setEditingLegalId(doc.id);
|
||
setLegalForm({ ...doc });
|
||
setLegalPreviewMode("edit");
|
||
}}
|
||
className="px-3 py-2 bg-indigo-600/20 hover:bg-indigo-600/30 text-indigo-300 hover:text-indigo-200 border border-indigo-500/30 rounded-lg text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1.5"
|
||
>
|
||
<Edit2 className="w-3.5 h-3.5" />
|
||
<span>Bearbeiten</span>
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteLegalDocument(doc.id)}
|
||
title="Dokument löschen"
|
||
className="p-2 bg-rose-950/40 hover:bg-rose-900/60 text-rose-400 hover:text-rose-200 border border-rose-900/50 rounded-lg transition-colors cursor-pointer"
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Assigned Profiles Badges */}
|
||
<div className="pt-2 border-t border-slate-800/60 flex flex-wrap items-center gap-2 text-xs">
|
||
<span className="text-[11px] font-mono text-slate-500 mr-1">Freigegeben für:</span>
|
||
{(["scifi", "erotica", "clara", "renee"] as const).map((pKey) => {
|
||
const isAssigned = doc.assignedProfiles?.includes(pKey);
|
||
return (
|
||
<span
|
||
key={pKey}
|
||
className={`px-2.5 py-0.5 rounded-full text-[11px] font-medium border ${
|
||
isAssigned
|
||
? "bg-indigo-950/60 border-indigo-700/60 text-indigo-300"
|
||
: "bg-slate-950/40 border-slate-800/60 text-slate-600 line-through"
|
||
}`}
|
||
>
|
||
{profileNamesMap[pKey]}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Short preview of content */}
|
||
<div className="p-3 bg-slate-950/60 rounded-xl border border-slate-800/40 text-xs text-slate-400 line-clamp-2 font-mono">
|
||
{doc.content ? doc.content.slice(0, 200) + (doc.content.length > 200 ? "..." : "") : "Kein Textinhalt hinterlegt."}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
/* Document Editor Form */
|
||
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-6 md:p-8 space-y-6">
|
||
<div className="flex items-center justify-between border-b border-slate-800 pb-4">
|
||
<div className="flex items-center gap-2">
|
||
<Scale className="w-5 h-5 text-indigo-400" />
|
||
<h4 className="text-lg font-bold text-white">
|
||
{editingLegalId === "new" ? "Neues Rechtsdokument anlegen" : `Dokument bearbeiten: ${legalForm.title || ""}`}
|
||
</h4>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 bg-slate-950 p-1 rounded-xl border border-slate-800">
|
||
<button
|
||
type="button"
|
||
onClick={() => setLegalPreviewMode("edit")}
|
||
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer flex items-center gap-1.5 ${
|
||
legalPreviewMode === "edit"
|
||
? "bg-indigo-600 text-white shadow-sm"
|
||
: "text-slate-400 hover:text-white"
|
||
}`}
|
||
>
|
||
<Edit2 className="w-3.5 h-3.5" />
|
||
<span>Bearbeiten</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setLegalPreviewMode("preview")}
|
||
className={`px-3 py-1 rounded-lg text-xs font-semibold transition-all cursor-pointer flex items-center gap-1.5 ${
|
||
legalPreviewMode === "preview"
|
||
? "bg-indigo-600 text-white shadow-sm"
|
||
: "text-slate-400 hover:text-white"
|
||
}`}
|
||
>
|
||
<Eye className="w-3.5 h-3.5" />
|
||
<span>Live-Vorschau</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Form fields */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||
<div className="space-y-1 sm:col-span-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Dokument-Titel</label>
|
||
<input
|
||
type="text"
|
||
value={legalForm.title || ""}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1 sm:col-span-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Dokumenttyp</label>
|
||
<select
|
||
value={legalForm.type || "impressum"}
|
||
onChange={(e) => setLegalForm({ ...legalForm, type: e.target.value as any })}
|
||
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"
|
||
>
|
||
<option value="impressum">Impressum (§ 5 DDG)</option>
|
||
<option value="privacy">Datenschutzerklärung (DSGVO)</option>
|
||
<option value="custom">Individuelles Rechtsdokument</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="space-y-1 sm:col-span-1">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Stand / Aktualisierungsdatum</label>
|
||
<input
|
||
type="text"
|
||
value={legalForm.updatedAt || ""}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Profile Assignment Selection */}
|
||
<div className="p-4 bg-slate-950/70 border border-slate-800 rounded-xl space-y-3">
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||
<div>
|
||
<h5 className="text-xs font-mono text-indigo-400 uppercase tracking-wider">
|
||
Freigabe für Autorenprofile & Seiten
|
||
</h5>
|
||
<p className="text-[11px] text-slate-400">
|
||
Wählen Sie aus, auf welchen Portfolioseiten dieses Dokument im Footer als Link eingeblendet wird.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleAssignAllProfiles(true)}
|
||
className="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white rounded-lg text-xs font-mono transition-colors cursor-pointer"
|
||
>
|
||
Alle auswählen
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => handleAssignAllProfiles(false)}
|
||
className="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white rounded-lg text-xs font-mono transition-colors cursor-pointer"
|
||
>
|
||
Keine
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 pt-2">
|
||
{[
|
||
{ key: "scifi" as const, name: authorData.scifi.name, badge: "Sci-Fi" },
|
||
{ key: "erotica" as const, name: authorData.erotica.name, badge: "Erotik" },
|
||
{ key: "clara" as const, name: authorData.clara.name, badge: "Naturlyrik" },
|
||
{ key: "renee" as const, name: authorData.renee.name, badge: "Romantik" }
|
||
].map((item) => {
|
||
const checked = legalForm.assignedProfiles?.includes(item.key);
|
||
return (
|
||
<div
|
||
key={item.key}
|
||
onClick={() => 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"
|
||
}`}
|
||
>
|
||
<div className="truncate">
|
||
<span className="text-[10px] font-mono text-indigo-400 block">{item.badge}</span>
|
||
<span className="text-xs font-bold truncate block">{item.name}</span>
|
||
</div>
|
||
{checked ? (
|
||
<CheckSquare className="w-4 h-4 text-indigo-400 shrink-0" />
|
||
) : (
|
||
<Square className="w-4 h-4 text-slate-600 shrink-0" />
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Text Editor or Preview */}
|
||
{legalPreviewMode === "edit" ? (
|
||
<div className="space-y-2">
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase flex items-center gap-1.5">
|
||
<span>Dokument-Inhalt (Markdown unterstützt)</span>
|
||
</label>
|
||
<div className="flex flex-wrap items-center gap-1.5 text-[11px]">
|
||
<span className="text-slate-500">Mustertexte einfügen:</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if (legalForm.content && !confirm("Aktuellen Inhalt durch das DDG-Impressum-Muster ersetzen?")) return;
|
||
setLegalForm({
|
||
...legalForm,
|
||
title: "Impressum",
|
||
type: "impressum",
|
||
content: `### Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG)
|
||
|
||
**Vor- und Nachname**
|
||
Straße und Hausnummer
|
||
PLZ Ort
|
||
Deutschland
|
||
|
||
### Kontakt
|
||
**E-Mail:** kontakt@example.org
|
||
**Website:** https://example.org
|
||
|
||
### Redaktionell verantwortlich
|
||
Vor- und Nachname
|
||
Straße und Hausnummer
|
||
PLZ Ort
|
||
|
||
### Urheberrecht
|
||
Die auf dieser Internetpräsenz präsentierten literarischen Werke und Inhalte sind urheberrechtlich geschützt.
|
||
|
||
### EU-Streitschlichtung
|
||
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit: https://ec.europa.eu/consumers/odr/
|
||
Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||
|
||
### Verbraucherstreitbeilegung / Universalschlichtungsstelle
|
||
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.`
|
||
});
|
||
}}
|
||
className="px-2 py-0.5 bg-slate-800 hover:bg-slate-700 text-indigo-300 rounded font-mono cursor-pointer"
|
||
>
|
||
+ Impressum-Muster
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if (legalForm.content && !confirm("Aktuellen Inhalt durch das DSGVO-Datenschutz-Muster ersetzen?")) return;
|
||
setLegalForm({
|
||
...legalForm,
|
||
title: "Datenschutzerklärung",
|
||
type: "privacy",
|
||
content: `### 1. Datenschutz auf einen Blick
|
||
|
||
**Allgemeine Hinweise**
|
||
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.
|
||
|
||
### 2. Verantwortliche Stelle
|
||
Verantwortlich für die Datenverarbeitung auf dieser Website ist:
|
||
|
||
**Vor- und Nachname**
|
||
Straße und Hausnummer
|
||
PLZ Ort
|
||
E-Mail: kontakt@example.org
|
||
|
||
### 3. Datenerfassung auf dieser Website
|
||
|
||
**Bereitstellung der Website und Erstellung der Logfiles**
|
||
Beim Aufrufen unserer Website werden durch den auf Ihrem Endgerät zum Einsatz kommenden Browser automatisch Informationen an den Server unserer Website gesendet. Diese Informationen werden temporär in einem sogenannten Logfile gespeichert:
|
||
- Browsertyp und Browserversion
|
||
- Verwendetes Betriebssystem
|
||
- Referrer URL (die zuvor besuchte Seite)
|
||
- Hostname des zugreifenden Rechners
|
||
- Uhrzeit der Serveranfrage
|
||
- IP-Adresse
|
||
|
||
Die Rechtsgrundlage für die Datenverarbeitung ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse zur Gewährleistung eines reibungslosen Verbindungsaufbaus und der Systemsicherheit).
|
||
|
||
**Kontaktaufnahme per E-Mail**
|
||
Wenn Sie uns per E-Mail Anfragen zukommen lassen, werden Ihre Angaben inklusive der von Ihnen dort angegebenen Kontaktdaten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei uns gespeichert. Diese Daten geben wir nicht ohne Ihre Einwilligung weiter (Art. 6 Abs. 1 lit. b bzw. lit. f DSGVO).
|
||
|
||
### 4. Eingebundene Dienste Dritter
|
||
|
||
**Spotify Player & Externe Buch-Links**
|
||
Auf einigen Seiten sind Hörbeispiele bzw. Playlists des Dienstes Spotify eingebunden oder Links zu Buchhändlern (z. B. Amazon, Thalia) gesetzt. Wenn Sie diese externen Links anklicken oder mit externen Playern interagieren, werden Daten an die jeweiligen Anbieter übertragen.
|
||
|
||
### 5. Ihre Rechte als betroffene Person
|
||
Sie haben im Rahmen der geltenden gesetzlichen Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger und den Zweck der Datenverarbeitung und ggf. ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten.`
|
||
});
|
||
}}
|
||
className="px-2 py-0.5 bg-slate-800 hover:bg-slate-700 text-emerald-300 rounded font-mono cursor-pointer"
|
||
>
|
||
+ Datenschutz-Muster
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<textarea
|
||
rows={14}
|
||
value={legalForm.content || ""}
|
||
onChange={(e) => setLegalForm({ ...legalForm, content: e.target.value })}
|
||
placeholder="Tragen Sie hier Ihren Gesetzestext oder Ihre Datenschutzerklärung ein..."
|
||
className="w-full px-4 py-3 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-xs font-mono leading-relaxed"
|
||
/>
|
||
<p className="text-[11px] text-slate-500">
|
||
Formatierungstipp: Überschriften mit <code className="text-slate-300">### Titel</code>, Fettgedruckt mit <code className="text-slate-300">**Text**</code>, Listen mit <code className="text-slate-300">- Punkt</code>, Links mit <code className="text-slate-300">[Titel](URL)</code>.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
/* Live Preview Box */
|
||
<div className="space-y-2">
|
||
<label className="text-xs font-mono text-slate-400 uppercase">Live-Vorschau der Darstellung:</label>
|
||
<div className="p-6 bg-slate-950 border border-slate-800 rounded-xl max-h-96 overflow-y-auto markdown-body text-slate-300 text-xs leading-relaxed space-y-3">
|
||
{legalForm.content ? (
|
||
<Markdown>{legalForm.content}</Markdown>
|
||
) : (
|
||
<p className="text-slate-500 italic">Noch kein Text zum Anzeigen vorhanden.</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-slate-800">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setEditingLegalId(null);
|
||
setLegalForm({});
|
||
}}
|
||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white rounded-xl text-xs font-semibold transition-colors cursor-pointer"
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleSaveLegalDocument}
|
||
disabled={loading || !legalForm.title?.trim()}
|
||
className="px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer disabled:opacity-50 shadow-lg shadow-indigo-950/50"
|
||
>
|
||
<Save className="w-4 h-4" />
|
||
<span>{loading ? "Speichern..." : "Dokument speichern"}</span>
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
}
|