- Add bioTag1, bioTag2, bioTag3 fields to AuthorProfile model and defaultData - Add interactive Schlagworte editing fields to AdminPanel - Update Renee, SciFi, Erotica, and Clara portfolio templates to render dynamic bio tags - Sanitize domain routing logic to automatically match www and naked domain variants
1504 lines
78 KiB
TypeScript
1504 lines
78 KiB
TypeScript
import React, { useState, useEffect, FormEvent } from "react";
|
|
import { AuthorData, AuthorProfile, Book, Project } from "../types";
|
|
import {
|
|
Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles,
|
|
BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight,
|
|
Palette, Type, Image as ImageIcon, Globe, Tag
|
|
} from "lucide-react";
|
|
import ImagePicker from "./ImagePicker";
|
|
|
|
interface AdminPanelProps {
|
|
onLogout: () => void;
|
|
}
|
|
|
|
export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
|
const [password, setPassword] = useState("");
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
const [token, setToken] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// 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")
|
|
const [activeTab, setActiveTab] = useState<"profile" | "texts" | "design" | "books" | "projects">("profile");
|
|
|
|
// Editing forms state
|
|
const [editingBookId, setEditingBookId] = useState<string | null>(null);
|
|
const [bookForm, setBookForm] = useState<Partial<Book>>({});
|
|
|
|
const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
|
|
const [projectForm, setProjectForm] = useState<Partial<Project>>({});
|
|
|
|
// 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("");
|
|
|
|
// Check login state on component mount
|
|
useEffect(() => {
|
|
const storedToken = localStorage.getItem("author_admin_token");
|
|
if (storedToken) {
|
|
setToken(storedToken);
|
|
setIsLoggedIn(true);
|
|
fetchAuthorData();
|
|
}
|
|
}, []);
|
|
|
|
const fetchAuthorData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/author-data");
|
|
const data = await res.json();
|
|
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 {
|
|
const res = await fetch("/api/admin/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ password }),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
localStorage.setItem("author_admin_token", data.token);
|
|
setToken(data.token);
|
|
setIsLoggedIn(true);
|
|
fetchAuthorData();
|
|
} else {
|
|
setError(data.error || "Ungültiges Passwort.");
|
|
}
|
|
} catch (err) {
|
|
setError("Verbindung zum Server fehlgeschlagen.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogoutLocal = () => {
|
|
localStorage.removeItem("author_admin_token");
|
|
setIsLoggedIn(false);
|
|
setToken("");
|
|
onLogout();
|
|
};
|
|
|
|
// Profile save handler
|
|
const handleSaveProfileInfo = async (profileKey: "scifi" | "erotica" | "clara" | "renee") => {
|
|
if (!authorData) return;
|
|
setError("");
|
|
setLoading(true);
|
|
|
|
try {
|
|
const res = await fetch("/api/admin/save-profile", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({
|
|
profileKey,
|
|
profileData: authorData[profileKey]
|
|
}),
|
|
});
|
|
|
|
const resData = await res.json();
|
|
if (res.ok && resData.success) {
|
|
alert("Profilinformationen erfolgreich gespeichert!");
|
|
fetchAuthorData();
|
|
} else {
|
|
setError(resData.error || "Profile konnte nicht gespeichert werden.");
|
|
}
|
|
} catch (err) {
|
|
setError("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
|
|
});
|
|
};
|
|
|
|
// --- 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 || ""
|
|
};
|
|
updatedProjects.push(newProj);
|
|
} else {
|
|
updatedProjects = updatedProjects.map(p =>
|
|
p.id === editingProjectId ? { ...p, ...projectForm } : p
|
|
);
|
|
}
|
|
|
|
const updatedProfile = { ...profile, projects: updatedProjects };
|
|
const nextData = { ...authorData, [activeProfile]: updatedProfile };
|
|
|
|
// Save to server
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/admin/save-profile", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }),
|
|
});
|
|
if (res.ok) {
|
|
setAuthorData(nextData);
|
|
setEditingProjectId(null);
|
|
setProjectForm({});
|
|
} else {
|
|
alert("Fehler beim Speichern des Projekts.");
|
|
}
|
|
} catch (err) {
|
|
alert("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 };
|
|
const nextData = { ...authorData, [activeProfile]: updatedProfile };
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/admin/save-profile", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }),
|
|
});
|
|
if (res.ok) {
|
|
setAuthorData(nextData);
|
|
} else {
|
|
alert("Fehler beim Löschen.");
|
|
}
|
|
} catch (err) {
|
|
alert("Netzwerkfehler.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// --- BOOK ACTIONS ---
|
|
const handleSaveBook = async () => {
|
|
if (!authorData || !profile) return;
|
|
|
|
let updatedBooks = [...profile.books];
|
|
if (editingBookId === "new") {
|
|
const newBook: Book = {
|
|
id: "book_" + Date.now(),
|
|
title: bookForm.title || "Unbenanntes Buch",
|
|
subtitle: bookForm.subtitle || "",
|
|
coverUrl: bookForm.coverUrl || "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400",
|
|
description: bookForm.description || "",
|
|
publishedYear: bookForm.publishedYear || new Date().getFullYear(),
|
|
buyLink: bookForm.buyLink || "",
|
|
spotifyPlaylistId: bookForm.spotifyPlaylistId || "",
|
|
genres: bookForm.genres || []
|
|
};
|
|
updatedBooks.push(newBook);
|
|
} else {
|
|
updatedBooks = updatedBooks.map(b =>
|
|
b.id === editingBookId ? { ...b, ...bookForm } as Book : b
|
|
);
|
|
}
|
|
|
|
const updatedProfile = { ...profile, books: updatedBooks };
|
|
const nextData = { ...authorData, [activeProfile]: updatedProfile };
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/admin/save-profile", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }),
|
|
});
|
|
if (res.ok) {
|
|
setAuthorData(nextData);
|
|
setEditingBookId(null);
|
|
setBookForm({});
|
|
} else {
|
|
alert("Fehler beim Speichern des Buches.");
|
|
}
|
|
} catch (err) {
|
|
alert("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 };
|
|
const nextData = { ...authorData, [activeProfile]: updatedProfile };
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch("/api/admin/save-profile", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }),
|
|
});
|
|
if (res.ok) {
|
|
setAuthorData(nextData);
|
|
} else {
|
|
alert("Fehler beim Löschen des Buches.");
|
|
}
|
|
} catch (err) {
|
|
alert("Netzwerkfehler.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// --- GEMINI SYNOPSIS GENERATOR ---
|
|
const handleGenerateAISynopsis = async () => {
|
|
if (!aiTitle.trim()) {
|
|
alert("Bitte geben Sie einen Buchtitel für den KI-Assistenten an.");
|
|
return;
|
|
}
|
|
setAiLoading(true);
|
|
setAiResult("");
|
|
try {
|
|
const res = await fetch("/api/admin/generate-blurb", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({
|
|
title: aiTitle,
|
|
genre: aiGenre || activeProfile === "scifi" ? "Science-Fiction" : "Sinnliche Literatur",
|
|
ideas: aiIdeas,
|
|
tone: aiTone
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.text) {
|
|
setAiResult(data.text);
|
|
setBookForm({
|
|
...bookForm,
|
|
description: data.text
|
|
});
|
|
} else {
|
|
alert(data.error || "Generierung fehlgeschlagen.");
|
|
}
|
|
} catch {
|
|
alert("Fehler bei der Verbindung zur KI-Schnittstelle.");
|
|
} finally {
|
|
setAiLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!isLoggedIn) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex items-center justify-center p-4">
|
|
<div className="w-full max-w-md p-8 bg-slate-900 border border-slate-800 rounded-2xl shadow-2xl space-y-6">
|
|
<div className="text-center space-y-2">
|
|
<div className="mx-auto w-12 h-12 bg-indigo-950/80 border border-indigo-700/50 rounded-xl flex items-center justify-center text-cyan-400">
|
|
<Lock className="w-6 h-6" />
|
|
</div>
|
|
<h1 className="text-2xl font-bold tracking-tight text-white">Autoren-Zentrale Admin</h1>
|
|
<p className="text-xs text-slate-400">Authentifizierung erforderlich. Passwortgeschütztes Kontrollzentrum.</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleLogin} className="space-y-4">
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-mono text-cyan-400 uppercase tracking-widest">Admin-Passwort</label>
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="Passwort eingeben..."
|
|
className="w-full px-4 py-3 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-cyan-400 transition-colors text-sm"
|
|
required
|
|
/>
|
|
<span className="text-[10px] text-slate-500">Standard-Passwort im Self-Host ist `autor2026`, anpassbar via Env-Variable `ADMIN_PASSWORD`.</span>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-rose-950/40 border border-rose-900/50 text-rose-300 text-xs rounded-lg text-center font-sans">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="w-full py-3 bg-gradient-to-r from-cyan-500 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-slate-950 font-bold rounded-xl transition-all flex items-center justify-center gap-2 text-sm cursor-pointer"
|
|
>
|
|
{loading ? <RefreshCw className="w-4 h-4 animate-spin text-slate-950" /> : <span>Zentrale aufschließen</span>}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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">
|
|
|
|
{/* 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 (Daniel Hesse)
|
|
</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 || "Annie Slone"})
|
|
</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 || "Clara Finch"})
|
|
</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 || "Renee Heart"})
|
|
</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}
|
|
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>
|
|
</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)}
|
|
token={token}
|
|
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. reneeheart.de, renee-heart.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">reneeheart.de, renee-heart.de</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>
|
|
</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">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">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)}
|
|
token={token}
|
|
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: "" });
|
|
}}
|
|
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>
|
|
|
|
<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: []
|
|
});
|
|
// 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">
|
|
<img src={b.coverUrl} alt={b.title} className="w-full h-full object-cover" referrerPolicy="no-referrer" />
|
|
</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">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);
|
|
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-450 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">
|
|
<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 })}
|
|
token={token}
|
|
label="Buch-Cover"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<label className="text-xs font-mono text-slate-400 uppercase">Veröffentlichungsjahr</label>
|
|
<input
|
|
type="number"
|
|
value={bookForm.publishedYear || ""}
|
|
onChange={(e) => setBookForm({ ...bookForm, publishedYear: parseInt(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 flex items-center gap-1">
|
|
<Eye className="w-3.5 h-3.5" />
|
|
<span>Buy-Link (z.B. Amazon, Thalia)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={bookForm.buyLink || ""}
|
|
onChange={(e) => setBookForm({ ...bookForm, buyLink: 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 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={bookForm.genres?.join(", ") || ""}
|
|
onChange={(e) => setBookForm({ ...bookForm, genres: e.target.value.split(",").map(g => g.trim()).filter(g => g !== "") })}
|
|
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"
|
|
/>
|
|
</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>
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|