diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9d3e26f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# Ignoriere lokale Daten und Abhängigkeiten beim Docker-Build +node_modules +dist +data +.git +.env +.env.* +*.log diff --git a/.gitignore b/.gitignore index 9c53d56..66f88ff 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,5 @@ coverage/ *.log .env* !.env.example -data/database.json +data/ diff --git a/public/books/slone/FestGehalten_web.png b/public/books/slone/FestGehalten_web.png deleted file mode 100644 index 5e9d72e..0000000 Binary files a/public/books/slone/FestGehalten_web.png and /dev/null differ diff --git a/public/books/slone/Zirkel_web.png b/public/books/slone/Zirkel_web.png deleted file mode 100644 index c120c8d..0000000 Binary files a/public/books/slone/Zirkel_web.png and /dev/null differ diff --git a/public/books/slone/ausgeliefert.png b/public/books/slone/ausgeliefert.png deleted file mode 100644 index c2cc0c5..0000000 Binary files a/public/books/slone/ausgeliefert.png and /dev/null differ diff --git a/public/books/slone/dreieck.png b/public/books/slone/dreieck.png deleted file mode 100644 index 86c8859..0000000 Binary files a/public/books/slone/dreieck.png and /dev/null differ diff --git a/public/books/slone/familienspiele.png b/public/books/slone/familienspiele.png deleted file mode 100644 index 7dc1f0b..0000000 Binary files a/public/books/slone/familienspiele.png and /dev/null differ diff --git a/public/books/slone/festgewickelt.png b/public/books/slone/festgewickelt.png deleted file mode 100644 index cd3dd37..0000000 Binary files a/public/books/slone/festgewickelt.png and /dev/null differ diff --git a/public/books/slone/geschwisterliebe.png b/public/books/slone/geschwisterliebe.png deleted file mode 100644 index e62558e..0000000 Binary files a/public/books/slone/geschwisterliebe.png and /dev/null differ diff --git a/public/books/slone/mutter.png b/public/books/slone/mutter.png deleted file mode 100644 index d61314d..0000000 Binary files a/public/books/slone/mutter.png and /dev/null differ diff --git a/public/books/slone/sabrina.png b/public/books/slone/sabrina.png deleted file mode 100644 index ea77198..0000000 Binary files a/public/books/slone/sabrina.png and /dev/null differ diff --git a/public/books/slone/zweifrauen.png b/public/books/slone/zweifrauen.png deleted file mode 100644 index 6d75b4c..0000000 Binary files a/public/books/slone/zweifrauen.png and /dev/null differ diff --git a/public/profiles/slone/photo.jpg b/public/profiles/slone/photo.jpg deleted file mode 100644 index 41c38fd..0000000 Binary files a/public/profiles/slone/photo.jpg and /dev/null differ diff --git a/public/profiles/slone/photo2.jpg b/public/profiles/slone/photo2.jpg deleted file mode 100644 index de63163..0000000 Binary files a/public/profiles/slone/photo2.jpg and /dev/null differ diff --git a/public/profiles/slone/photo3.jpg b/public/profiles/slone/photo3.jpg deleted file mode 100644 index 7b1b3f2..0000000 Binary files a/public/profiles/slone/photo3.jpg and /dev/null differ diff --git a/server.ts b/server.ts index 317a1ab..ec19b60 100644 --- a/server.ts +++ b/server.ts @@ -5,7 +5,7 @@ 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"; +import { AuthorData, AuthorProfile } from "./src/types.js"; dotenv.config(); @@ -32,36 +32,43 @@ async function initDatabase(): Promise { try { const existingData = await fs.readFile(DATA_FILE, "utf-8"); const parsed = JSON.parse(existingData) as AuthorData; - - // Auto-heal database: make sure all profiles from defaultAuthorData exist let modified = false; - if (!parsed.scifi) { - parsed.scifi = defaultAuthorData.scifi; - modified = true; - } - if (!parsed.erotica) { - parsed.erotica = defaultAuthorData.erotica; - modified = true; - } - if (!parsed.clara) { - parsed.clara = defaultAuthorData.clara; - modified = true; + + // Ensure all dynamic static text and design properties exist for all profiles + const profileKeys: ("scifi" | "erotica" | "clara" | "renee")[] = ["scifi", "erotica", "clara", "renee"]; + for (const pKey of profileKeys) { + if (!parsed[pKey]) { + parsed[pKey] = defaultAuthorData[pKey]; + modified = true; + } else { + const defaults = defaultAuthorData[pKey]; + const keysToEnsure: (keyof AuthorProfile)[] = [ + "customSectionTitle", "customSectionContent", + "customDomain", "customPath", + "bioTag1Label", "bioTag1Value", + "bioTag2Label", "bioTag2Value", + "bioTag3Label", "bioTag3Value", + "badgeText", "aboutTitle", "projectsTitle", "projectsSubtitle", + "booksTitle", "booksSubtitle", "footerText", + "accentColor", "secondaryColor", "backgroundColor", "cardBgColor", + "fontFamily", "heroBannerUrl" + ]; + for (const k of keysToEnsure) { + if (parsed[pKey][k] === undefined) { + (parsed[pKey] as any)[k] = defaults[k]; + modified = true; + } + } + } } - // Ensure customSection fields exist for all profiles - if (parsed.scifi && parsed.scifi.customSectionTitle === undefined) { - parsed.scifi.customSectionTitle = defaultAuthorData.scifi.customSectionTitle; - parsed.scifi.customSectionContent = defaultAuthorData.scifi.customSectionContent; - modified = true; - } - if (parsed.erotica && parsed.erotica.customSectionTitle === undefined) { - parsed.erotica.customSectionTitle = defaultAuthorData.erotica.customSectionTitle; - parsed.erotica.customSectionContent = defaultAuthorData.erotica.customSectionContent; - modified = true; - } - if (parsed.clara && parsed.clara.customSectionTitle === undefined) { - parsed.clara.customSectionTitle = defaultAuthorData.clara.customSectionTitle; - parsed.clara.customSectionContent = defaultAuthorData.clara.customSectionContent; + // Migration check: update erotica default name from M. S. Velvet / Marc Velvet to Annie Slone if unchanged + if (parsed.erotica && (parsed.erotica.name === "M. S. Velvet" || parsed.erotica.name === "Marc Velvet")) { + parsed.erotica.name = "Annie Slone"; + parsed.erotica.heroTitle = "ANNIE SLONE"; + if (parsed.erotica.bio && parsed.erotica.bio.includes("M. S. Velvet")) { + parsed.erotica.bio = parsed.erotica.bio.replace(/M\. S\. Velvet/g, "Annie Slone"); + } modified = true; } @@ -154,7 +161,7 @@ app.post("/api/admin/login", (req, res) => { // 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" && profileKey !== "clara") { + if (profileKey !== "scifi" && profileKey !== "erotica" && profileKey !== "clara" && profileKey !== "renee") { res.status(400).json({ error: "Ungültiger Profilschlüssel." }); return; } diff --git a/src/App.tsx b/src/App.tsx index 0a16a4c..4c4c836 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { AuthorData } from "./types"; import SciFiPortfolio from "./components/SciFiPortfolio"; import EroticaPortfolio from "./components/EroticaPortfolio"; import ClaraPortfolio from "./components/ClaraPortfolio"; +import ReneePortfolio from "./components/ReneePortfolio"; import AdminPanel from "./components/AdminPanel"; import { Lock, Eye, Sparkles, RefreshCw, Leaf } from "lucide-react"; @@ -39,29 +40,53 @@ export default function App() { }, []); // Ermittelt die aktive Suite basierend auf Pfad-Überschreibungen oder dem Domainnamen (Hostname) - const getActiveSuite = (): "erotica" | "clara" | "scifi" => { + const getActiveSuite = (): "erotica" | "clara" | "scifi" | "renee" => { const hostname = window.location.hostname.toLowerCase(); + const profileKeys: ("erotica" | "clara" | "renee" | "scifi")[] = ["erotica", "clara", "renee", "scifi"]; - // 1. Pfad-basierte Routen (haben Vorrang, z.B. für die Vorschau oder Admin-Interaktionen) - if (currentPath === "/sensual-moments" || currentPath.startsWith("/sensual-moments/")) { - return "erotica"; - } - if (currentPath === "/clara-finch" || currentPath.startsWith("/clara-finch/")) { - return "clara"; - } - if (currentPath === "/sci-fi" || currentPath.startsWith("/sci-fi/")) { - return "scifi"; + // 1. Pfad-basierte Routen (haben Vorrang, z.B. für Vorschau & Direktlinks) + for (const key of profileKeys) { + const p = authorData?.[key]; + const customPaths: string[] = p?.customPath + ? p.customPath.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) + : []; + + // Statische Standard-Fallbacks + if (key === "erotica") customPaths.push("/sensual-moments", "/annie-slone", "/marc-velvet"); + if (key === "clara") customPaths.push("/clara-finch", "/clara"); + if (key === "renee") customPaths.push("/renee-heart", "/renee"); + if (key === "scifi") customPaths.push("/sci-fi", "/daniel-hesse"); + + for (const prefix of customPaths) { + if (prefix) { + const formattedPrefix = prefix.startsWith("/") ? prefix : "/" + prefix; + if (currentPath === formattedPrefix || currentPath.startsWith(formattedPrefix + "/")) { + return key; + } + } + } } - // 2. Domain-basierte Routen (für den Live-Betrieb hinter einem Reverse Proxy) - if (hostname.includes("annieslone.de")) { - return "erotica"; - } - if (hostname.includes("clarafinch.de")) { - return "clara"; - } - if (hostname.includes("hesse-sf.de")) { - return "scifi"; + // 2. Dynamische Domain-basierte Routen (für Live-Domains) + for (const key of profileKeys) { + const p = authorData?.[key]; + const customDomains: string[] = p?.customDomain + ? p.customDomain.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) + : []; + + // Statische Standard-Fallbacks + if (key === "erotica") customDomains.push("annieslone.de", "marcvelvet.de"); + if (key === "clara") customDomains.push("clarafinch.de", "clara-finch.de"); + if (key === "renee") customDomains.push("reneeheart.de", "renee-heart.de"); + if (key === "scifi") customDomains.push("hesse-sf.de", "danielhesse.de"); + + for (const domain of customDomains) { + // Entferne evtl. eingegebene Protokolle (http://, https://) oder führendes "www." + const cleanDomain = domain.replace(/^https?:\/\//, "").replace(/^www\./, "").trim(); + if (cleanDomain && (hostname === cleanDomain || hostname.includes(cleanDomain))) { + return key; + } + } } // Standard-Fallback (Daniel Hesse SciFi) @@ -81,9 +106,11 @@ export default function App() { const activeSuite = getActiveSuite(); if (activeSuite === "erotica") { - document.title = authorData.erotica.name || "Marc Velvet"; + document.title = authorData.erotica.name || "Annie Slone"; } else if (activeSuite === "clara") { document.title = authorData.clara.name || "Clara Finch"; + } else if (activeSuite === "renee") { + document.title = authorData.renee.name || "Renee Heart"; } else { document.title = authorData.scifi.name || "Daniel Hesse"; } @@ -123,10 +150,7 @@ export default function App() { return ; } - // 2. Sensual/Erotica Portfolio pseudonym - // 3. Poetic Clara Finch Portfolio pseudonym - // 4. SciFi Daniel Hesse Portfolio - + // Render active portfolio const renderActivePortfolio = () => { const activeSuite = getActiveSuite(); if (activeSuite === "erotica") { @@ -135,6 +159,9 @@ export default function App() { if (activeSuite === "clara") { return ; } + if (activeSuite === "renee") { + return ; + } return ; }; diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index 1df3749..3c231b3 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -2,7 +2,8 @@ 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 + BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight, + Palette, Type, Image as ImageIcon, Globe, Tag } from "lucide-react"; import ImagePicker from "./ImagePicker"; @@ -20,11 +21,11 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { // Author Data State const [authorData, setAuthorData] = useState(null); - // Selected Profile state ("scifi" vs "erotica" vs "clara") - const [activeProfile, setActiveProfile] = useState<"scifi" | "erotica" | "clara">("scifi"); + // Selected Profile state ("scifi" vs "erotica" vs "clara" vs "renee") + const [activeProfile, setActiveProfile] = useState<"scifi" | "erotica" | "clara" | "renee">("scifi"); - // Sub-tab selection state ("profile" | "books" | "projects") - const [activeTab, setActiveTab] = useState<"profile" | "books" | "projects">("profile"); + // 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(null); @@ -98,7 +99,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { }; // Profile save handler - const handleSaveProfileInfo = async (profileKey: "scifi" | "erotica" | "clara") => { + const handleSaveProfileInfo = async (profileKey: "scifi" | "erotica" | "clara" | "renee") => { if (!authorData) return; setError(""); setLoading(true); @@ -411,24 +412,30 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
{/* Swapping genre profile profile */} -
+
+
@@ -472,6 +479,20 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { Profil & Biografie info + +
+ {/* Dynamic Schlagworte / Highlights */} +
+
+

+ + Schlagworte & Fokus (3 Infoboxen unter "Über mich") +

+

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.

+
+ +
+ {/* Schlagwort 1 */} +
+
+ Schlagwort 1 +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + {/* Schlagwort 2 */} +
+
+ Schlagwort 2 +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + {/* Schlagwort 3 */} +
+
+ Schlagwort 3 +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+
+
+ + {/* Custom Domain & Routing Section */} +
+
+

+ + Domain- & Routing-Einstellungen +

+

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.

+
+ +
+
+ + 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" + /> +

Kommagetrennte Domainnamen. Bsp: reneeheart.de, renee-heart.de

+
+ +
+ + 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" + /> +

Kommagetrennte Pfade für Direktlinks & Vorschau. Bsp: /renee-heart, /renee

+
+
+
+ {/* Custom Highlight Section */}
@@ -595,7 +747,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { />
- +