diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7a550fe --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# GEMINI_API_KEY: Required for Gemini AI API calls. +# AI Studio automatically injects this at runtime from user secrets. +# Users configure this via the Secrets panel in the AI Studio UI. +GEMINI_API_KEY="MY_GEMINI_API_KEY" + +# APP_URL: The URL where this applet is hosted. +# AI Studio automatically injects this at runtime with the Cloud Run service URL. +# Used for self-referential links, OAuth callbacks, and API endpoints. +APP_URL="MY_APP_URL" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a86d2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +dist/ +coverage/ +.DS_Store +*.log +.env* +!.env.example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..68a591f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Build Stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy dependency manifests +COPY package*.json ./ + +# Install all dependencies +RUN npm ci + +# Copy full application codebase +COPY . . + +# Compile frontend and bundle backend files +RUN npm run build + +# Production alpine stage +FROM node:20-alpine + +WORKDIR /app + +# Copy configuration files +COPY package*.json ./ + +# Install only production dependencies (Express, dotenv, @google/genai) +RUN npm ci --only=production + +# Copy compiled build resources from builder stage +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/data ./data + +# Expose port (Internal container routing to 3000) +EXPOSE 3000 + +ENV NODE_ENV=production +ENV ADMIN_PASSWORD=autor2026 + +# Execute standalone node production entrypoint +CMD ["node", "dist/server.cjs"] diff --git a/README.md b/README.md index 2241000..b0186f4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,92 @@ -
+# 📚 Dual Identity Autoren-Zentrale (Selbstgehostet) -GHBanner +Willkommen in Ihrer modernen, reaktiven Autoren-Website für das 21. Jahrhundert! Dieses System ermöglicht es Ihnen, zwei völlig eigenständige Autorenprofile mit individuellen Themen, Biografien, Schreibprojekten und Bücherregalen über eine einzige, passwortgeschützte Administrationsoberfläche zu pflegen. -

Built with AI Studio

+--- -

The fastest path from prompt to production with Gemini.

+## 🛠️ Warum kein klassischer LAMP-Stack? (Unser Vorschlag) - Start building +Bisher liefen klassische Portfolios oft auf einem standardmäßigen **LAMP-Stack** (Linux, Apache, MySQL, PHP). Für diese modernisierte Website schlagen wir einen **Node.JS + Docker-Stack** vor. Hier ist der Grund, warum das für Ihre Bedürfnisse die deutlich bessere Option ist: -
+1. **Vollständige Kapselung (Kein Server-Wildwuchs):** Bei einem LAMP-Stack müssen Sie Apache/Nginx, PHP-Laufzeiten und eine MySQL-Datenbank auf dem Host installieren und managen. Unser Docker-Stack bündelt das gesamte Frontend, das Express-Backend und die Speicherrelevanz in einem **einzigen, autarken Container**. +2. **Blitzschnelle, reaktive Benutzeroberfläche (SPA):** Das Frontend nutzt modernste Webtechnologie (React & Tailwind CSS). Seitenwechsel und Buchdetails laden augenblicklich im Browser des Nutzers, ohne das klassische, träge Neuladen von PHP-Seiten. +3. **Einfachheit bei Datensicherungen (Backups):** Anstatt komplexe MySQL-Dumps durchzuführen, speichert unsere Anwendung alle Buchtitel, Projekte, Cover und Biografien in einer schlanken, robusten JSON-Datenbankdatei (`/data/database.json`). Ein Backup lässt sich durch einfaches Kopieren dieser Datei anfertigen! +4. **KI-Unterstützung integriert:** Der integrierte **Gemini-Klappentext-Assistent** läuft nahtlos und sicher über das Node-Backend (ohne API-Schlüssel im Webbrowser zu exponieren). + +--- + +## 🌌 Die Trennung der Profile + +Das System ist von Grund auf so strukturiert, dass Besucher der Website **keine Verbindung** zwischen Ihren beiden Identitäten herstellen können: +- **Echte Identität (Daniel Hesse — Sci-Fi & Fantasy):** Erreichbar auf der Standard-Startseite (`/`). Das Design kombiniert dunkelblaue Weltraum-Energie, Monospace-Einflüsse und kosmische Magie-Akzente. +- **Pseudonym (M. S. Velvet — Sinnliche Literatur):** Erreichbar **ausschließlich** über den Pfad `/sensual-moments`. Sie verwendet ein warmes, edles "Champagner-Gold & Samt-Bordeaux"-Thema mit klassischer und anspruchsvoller Serif-Asthetik. Es gibt keinerlei gegenseitige Verlinkungen! + +--- + +## 🚀 Inbetriebnahme als Docker-Stack (Empfohlen) + +Es wird dringend empfohlen, die Website mithilfe von **Docker und Docker-Compose** zu betreiben. Dies garantiert, dass die Anwendung sofort und unabhängig von installierten Node.JS-Versionen auf Ihrem Server funktioniert. + +### 1. Dateien vorbereiten +Stellen Sie sicher, dass sich folgende Dateien im gleichen Ordner auf Ihrem Server befinden: +- `Dockerfile` +- `docker-compose.yml` +- Der gesamte Code-Ordner + +### 2. Konfiguration anpassen +Öffnen Sie die `docker-compose.yml` auf Ihrem Server und passen Sie folgende Umgebungsvariablen an: +```yaml +environment: + - NODE_ENV=production + # Das Passwort für Ihren Administrationsbereich (/admin): + - ADMIN_PASSWORD=IhrSicheresLieblingsPasswort123 + # (Optional) Für den 21. Jahrhundert Schreibassistenten (Gemini 3.5 Flash) + - GEMINI_API_KEY=Ihr_Gemini_API_Schluessel +``` + +### 3. Container starten +Führen Sie im entsprechenden Verzeichnis folgenden Befehl aus: +```bash +docker compose up -d --build +``` +Die Anwendung baut das Image und startet die Autoren-Zentrale im Hintergrund. Sie ist nun direkt auf Port **`3000`** Ihres Webservers erreichbar! + +#### Reverse Proxy Tipp: +Sie können ganz hervorragend einen Reverse Proxy wie **Nginx Proxy Manager**, **Traefik** oder **Caddy** davorhängen, um SSL-Zertifikate (Let's Encrypt) zuzuweisen und Ihre Domain auf den Container-Port `3000` umzuleiten. + +--- + +## 💾 Manuelle Installation ohne Docker (Alternativ) + +Sollten Sie die Software direkt auf Ihrem Server (ohne Docker) starten wollen: + +1. **Voraussetzung:** Installieren Sie **Node.js** (v18 oder neuer) auf Ihrem Linux- oder Windows-Server. +2. **Abhängigkeiten installieren:** + ```bash + npm install + ``` +3. **Produktions-Build ausführen:** + ```bash + npm run build + ``` +4. **Umgebungsvariablen setzen:** + Erstellen Sie eine `.env`-Datei oder exportieren Sie diese im Terminal: + ```bash + export ADMIN_PASSWORD="IhrSicheresPasswort" + export GEMINI_API_KEY="Ihr_Gemini_API_Schlüssel" + ``` +5. **Starten:** + ```bash + npm start + ``` + +--- + +## 🧱 Datensicherung / Backups + +Alle Ihre wertvollen Inhalte (Biografien, Projektfortschritte und alle Bücherregal-Einträge) liegen verschlüsselt strukturiert in Ihrem Projektordner unter: +`./data/database.json` + +Da wir in der `docker-compose.yml` ein **lokales Volume** gemountet haben (`./data:/app/data`), wird diese Datei direkt auf die Festplatte Ihres Servers geschrieben. +- **Backup erstellen:** Sichern Sie einfach die Datei `./data/database.json`. +- **Wiederherstellen:** Platzieren Sie eine gesicherte `database.json` in den `./data/`-Ordner vor dem Starten des Containers. diff --git a/assets/.aistudio/.gitignore b/assets/.aistudio/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/assets/.aistudio/.gitignore @@ -0,0 +1 @@ +* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..16119fa --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.8" + +services: + author-suite: + build: + context: . + dockerfile: Dockerfile + container_name: author_portfolio_suite + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - ./data:/app/data + environment: + - NODE_ENV=production + # Ändern Sie das Passwort für Ihren Administrationsbereich: + - ADMIN_PASSWORD=IhrSicheresPasswort2026 + # (Optional) Für den 21. Jahrhundert KI-Klappentextassistenten (Gemini 3.5 Flash) + - GEMINI_API_KEY=Ihr_Gemini_API_Schluessel diff --git a/index.html b/index.html new file mode 100644 index 0000000..21dfe69 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + My Google AI Studio App + + +
+ + + + diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..4ec7c95 --- /dev/null +++ b/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "Dual Identity Author Portfolio", + "description": "A high-performance modern portfolio for authors writing under multiple pen names. Completely separate visual themes, isolated routes, integrated Spotify player embeds, and an administrative panel. Docker self-hosting ready.", + "requestFramePermissions": [], + "majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8f06d77 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "react-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "tsx server.ts", + "build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs", + "start": "node dist/server.cjs", + "clean": "rm -rf dist", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@google/genai": "^2.4.0", + "@tailwindcss/vite": "^4.1.14", + "@vitejs/plugin-react": "^5.0.4", + "lucide-react": "^0.546.0", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "vite": "^6.2.3", + "express": "^4.21.2", + "dotenv": "^17.2.3", + "motion": "^12.23.24" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "esbuild": "^0.25.0", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.3", + "@types/express": "^4.17.21" + } +} diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..118823e --- /dev/null +++ b/server.ts @@ -0,0 +1,186 @@ +import express from "express"; +import path from "path"; +import fs from "fs/promises"; +import { fileURLToPath } from "url"; +import dotenv from "dotenv"; +import { GoogleGenAI } from "@google/genai"; +import { defaultAuthorData } from "./src/defaultData.js"; +import { AuthorData } from "./src/types.js"; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +const PORT = 3000; + +// Path to durable local database file +const DATA_DIR = path.join(process.cwd(), "data"); +const DATA_FILE = path.join(DATA_DIR, "database.json"); + +// Establish initial database of authors +async function initDatabase(): Promise { + try { + await fs.mkdir(DATA_DIR, { recursive: true }); + try { + const existingData = await fs.readFile(DATA_FILE, "utf-8"); + return JSON.parse(existingData) as AuthorData; + } catch { + // If file doesn't exist, seed it with default data + await fs.writeFile(DATA_FILE, JSON.stringify(defaultAuthorData, null, 2), "utf-8"); + return defaultAuthorData; + } + } catch (err) { + console.error("Database storage initialization failed, using default data in-memory:", err); + return defaultAuthorData; + } +} + +// In-memory runtime data cache, synced to active storage file +let dbCache: AuthorData; + +// Initialize on boot +initDatabase().then((data) => { + dbCache = data; +}); + +// Configure middleware +app.use(express.json({ limit: "10mb" })); + +// Initialize Google GenAI if API key exists +const getGeminiClient = () => { + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey) return null; + return new GoogleGenAI({ + apiKey, + httpOptions: { + headers: { + "User-Agent": "aistudio-build", + }, + }, + }); +}; + +// Admin authentication password helper +// In production or self-hosted, they set ADMIN_PASSWORD in environment or docker-compose. +// Default fallback is "autor2026" +const getAdminPassword = () => { + return process.env.ADMIN_PASSWORD || "autor2026"; +}; + +// Authorization verification middleware +const verifyToken = (req: express.Request, res: express.Response, next: express.NextFunction) => { + const authHeader = req.headers.authorization; + if (!authHeader) { + res.status(401).json({ error: "Kein Autorisierungs-Token bereitgestellt." }); + return; + } + const token = authHeader.replace("Bearer ", ""); + // To keep session simple, secure, and self-hosted, our auth token is just the password itself or adminPassword + if (token === getAdminPassword()) { + next(); + } else { + res.status(403).json({ error: "Ungültiges Passwort oder Sitzungstoken." }); + } +}; + +// --- API ROUTES --- + +// 1. Get entire public author setup +app.get("/api/author-data", (req, res) => { + res.json(dbCache || defaultAuthorData); +}); + +// 2. Manage Admin Login +app.post("/api/admin/login", (req, res) => { + const { password } = req.body; + if (!password) { + res.status(400).json({ error: "Passwort ist erforderlich." }); + return; + } + + if (password === getAdminPassword()) { + // Return the token which client stores in localStorage + res.json({ success: true, token: getAdminPassword() }); + } else { + res.status(401).json({ error: "Ungültiges Passwort. Bitte versuchen Sie es erneut." }); + } +}); + +// 3. Save modified profile configurations (About, Projects, Books) +app.post("/api/admin/save-profile", verifyToken, async (req, res) => { + const { profileKey, profileData } = req.body; + if (profileKey !== "scifi" && profileKey !== "erotica") { + res.status(400).json({ error: "Ungültiger Profilschlüssel." }); + return; + } + + try { + dbCache[profileKey] = profileData; + await fs.writeFile(DATA_FILE, JSON.stringify(dbCache, null, 2), "utf-8"); + res.json({ success: true, message: "Profil erfolgreich gespeichert." }); + } catch (err: any) { + console.error("Failed to write to database.json:", err); + res.status(500).json({ error: "Fehler beim persistenten Speichern der Formulardaten." }); + } +}); + +// 4. Creative AI Blurb Assistant for book blurb updates +app.post("/api/admin/generate-blurb", verifyToken, async (req, res) => { + const { title, genre, ideas, tone } = req.body; + + const ai = getGeminiClient(); + if (!ai) { + res.status(503).json({ + error: "Mit dem integrierten KI-Schreibassistenten konnte keine Verbindung hergestellt werden. Bitte stellen Sie sicher, dass GEMINI_API_KEY konfiguriert ist." + }); + return; + } + + try { + const prompt = `Du bist ein professioneller literarischer Marketing-Experte und Buch-Co-Autor. +Schreibe eine fesselnde, hochkarätige Synopsis (Buchrückenblurb) auf Deutsch für folgendes Buch: +- Buchtitel: "${title}" +- Genre/Kategorie: "${genre}" +- Eingebundene Ideen/Motive: "${ideas}" +- Gewünschte Tonalität/Stilrichtung: "${tone}" + +Die Synopsis soll neugierig machen, stark die visuelle Stimmung rüberbringen und frei von Klischees sein. Teile den Text in zwei bis max. drei Absätze auf, evtl. eingeleitet durch einen kurzen, fetten Einzeiler, der Aufmerksamkeit catched (z.B. ein prägnanter Satz). Antworte NUR mit dem generierten Blurb-Text in Deutsch.`; + + const response = await ai.models.generateContent({ + model: "gemini-3.5-flash", + contents: prompt, + }); + + const generatedText = response.text || "Fehler beim Generieren der Synopsis."; + res.json({ success: true, text: generatedText }); + } catch (error: any) { + console.error("Gemini-Fehler aufgetreten:", error); + res.status(500).json({ error: "KI-Generierungsfehler: " + error.message }); + } +}); + +// Configure Vite middleware or static serve +async function startServer() { + if (process.env.NODE_ENV !== "production") { + const { createServer: createViteServer } = await import("vite"); + const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: "spa", + }); + app.use(vite.middlewares); + } else { + const distPath = path.join(process.cwd(), "dist"); + app.use(express.static(distPath)); + app._router.get("*", (req: express.Request, res: express.Response) => { + res.sendFile(path.join(distPath, "index.html")); + }); + } + + app.listen(PORT, "0.0.0.0", () => { + console.log(`Server running on http://0.0.0.0:${PORT}`); + }); +} + +startServer(); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..10fd809 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,114 @@ +import { useState, useEffect } from "react"; +import { AuthorData } from "./types"; +import SciFiPortfolio from "./components/SciFiPortfolio"; +import EroticaPortfolio from "./components/EroticaPortfolio"; +import AdminPanel from "./components/AdminPanel"; +import { Lock, Eye, Sparkles, RefreshCw } from "lucide-react"; + +export default function App() { + const [authorData, setAuthorData] = useState(null); + const [loading, setLoading] = useState(true); + const [currentPath, setCurrentPath] = useState(window.location.pathname); + + // Monitor location changes for custom routing + useEffect(() => { + const handleLocationChange = () => { + setCurrentPath(window.location.pathname); + }; + + // Listen to popstate for navigation back/forward support + window.addEventListener("popstate", handleLocationChange); + return () => { + window.removeEventListener("popstate", handleLocationChange); + }; + }, []); + + // Fetch profiles configuration from server + useEffect(() => { + fetch("/api/author-data") + .then((res) => res.json()) + .then((data) => { + setAuthorData(data); + setLoading(false); + }) + .catch((err) => { + console.error("Fehler beim Abrufen der Autoren-Informationen:", err); + setLoading(false); + }); + }, []); + + const handleAdminLogout = () => { + // Relocate to landing site after administration work + window.history.pushState({}, "", "/"); + setCurrentPath("/"); + }; + + if (loading) { + return ( +
+ +

Kryptografisches Terminal bootet...

+
+ ); + } + + if (!authorData) { + return ( +
+
+

Verbindungsfehler

+

+ Die Verbindung zum lokalen Datenspeicher-Server konnte nicht hergestellt werden. Bitte vergewissern Sie sich, dass Ihr Express API-Server ordnungsgemäß läuft. +

+
+
+ ); + } + + // Purely route-based render isolation structure + // 1. Administration panel + if (currentPath === "/admin" || currentPath.startsWith("/admin/")) { + return ; + } + + // 2. Sensual/Erotica Portfolio pseudonym - accessed ONLY via /sensual-moments (Strict content separation) + if (currentPath === "/sensual-moments" || currentPath.startsWith("/sensual-moments/")) { + return ; + } + + // 3. Fallback: SciFi & Fantasy portfolio - accessed via `/` (Daniel Hesse identity) + return ( + <> + + + {/* Absolute floating administration bypass link hidden on the bottom right for convenience of testing/admin accessing */} +
+ { + e.preventDefault(); + window.history.pushState({}, "", "/admin"); + setCurrentPath("/admin"); + }} + className="text-slate-500 hover:text-cyan-400 font-bold flex items-center gap-1" + > + + Admin-Zentrale + + + + ); +} diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx new file mode 100644 index 0000000..1707afa --- /dev/null +++ b/src/components/AdminPanel.tsx @@ -0,0 +1,1001 @@ +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 +} from "lucide-react"; + +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(null); + + // Selected Profile state ("scifi" vs "erotica") + const [activeProfile, setActiveProfile] = useState<"scifi" | "erotica">("scifi"); + + // Sub-tab selection state ("profile" | "books" | "projects") + const [activeTab, setActiveTab] = useState<"profile" | "books" | "projects">("profile"); + + // Editing forms state + const [editingBookId, setEditingBookId] = useState(null); + const [bookForm, setBookForm] = useState>({}); + + const [editingProjectId, setEditingProjectId] = useState(null); + const [projectForm, setProjectForm] = useState>({}); + + // 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") => { + 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 ( +
+
+
+
+ +
+

Autoren-Zentrale Admin

+

Authentifizierung erforderlich. Passwortgeschütztes Kontrollzentrum.

+
+ +
+
+ + 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 + /> + Standard-Passwort im Self-Host ist `autor2026`, anpassbar via Env-Variable `ADMIN_PASSWORD`. +
+ + {error && ( +
+ {error} +
+ )} + + +
+
+
+ ); + } + + if (!authorData || !profile) { + return ( +
+ + Lade Portfolio-Strukturen und Datenbank... +
+ ); + } + + return ( +
+ + {/* Top Navbar */} + + + {/* Main Admin Workspace Container */} +
+ + {/* Left Side: Navigation Links & Theme Context info */} +
+
+

Aktive Identität

+
+ {profile.name} +
+

{profile.name}

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

+ + KI-Kopierassistent +

+

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

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

Profil & Biografie verwalten

+

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

+
+ +
+ +
+
+ + updateProfileField("name", e.target.value)} + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" + /> +
+
+ + updateProfileField("avatarUrl", e.target.value)} + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" + /> +
+
+ + updateProfileField("heroTitle", e.target.value)} + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" + /> +
+
+ + updateProfileField("heroSubtitle", e.target.value)} + className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" + /> +
+
+ +