Modularization done #9

Merged
Dada1981 merged 3 commits from dev into main 2026-07-26 19:36:20 +00:00
9 changed files with 256 additions and 53 deletions
Showing only changes of commit 99ea450c57 - Show all commits

View file

@ -44,6 +44,7 @@ async function initDatabase(): Promise<AuthorData> {
const defaults = defaultAuthorData[pKey]; const defaults = defaultAuthorData[pKey];
const keysToEnsure: (keyof AuthorProfile)[] = [ const keysToEnsure: (keyof AuthorProfile)[] = [
"customSectionTitle", "customSectionContent", "customSectionTitle", "customSectionContent",
"customDomain", "customPath",
"badgeText", "aboutTitle", "projectsTitle", "projectsSubtitle", "badgeText", "aboutTitle", "projectsTitle", "projectsSubtitle",
"booksTitle", "booksSubtitle", "footerText", "booksTitle", "booksSubtitle", "footerText",
"accentColor", "secondaryColor", "backgroundColor", "cardBgColor", "accentColor", "secondaryColor", "backgroundColor", "cardBgColor",

View file

@ -42,33 +42,51 @@ export default function App() {
// Ermittelt die aktive Suite basierend auf Pfad-Überschreibungen oder dem Domainnamen (Hostname) // Ermittelt die aktive Suite basierend auf Pfad-Überschreibungen oder dem Domainnamen (Hostname)
const getActiveSuite = (): "erotica" | "clara" | "scifi" | "renee" => { const getActiveSuite = (): "erotica" | "clara" | "scifi" | "renee" => {
const hostname = window.location.hostname.toLowerCase(); 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) // 1. Pfad-basierte Routen (haben Vorrang, z.B. für Vorschau & Direktlinks)
if (currentPath === "/sensual-moments" || currentPath.startsWith("/sensual-moments/")) { for (const key of profileKeys) {
return "erotica"; const p = authorData?.[key];
} const customPaths: string[] = p?.customPath
if (currentPath === "/clara-finch" || currentPath.startsWith("/clara-finch/")) { ? p.customPath.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
return "clara"; : [];
}
if (currentPath === "/renee-heart" || currentPath.startsWith("/renee-heart/") || currentPath === "/renee" || currentPath.startsWith("/renee/")) { // Statische Standard-Fallbacks
return "renee"; if (key === "erotica") customPaths.push("/sensual-moments", "/annie-slone", "/marc-velvet");
} if (key === "clara") customPaths.push("/clara-finch", "/clara");
if (currentPath === "/sci-fi" || currentPath.startsWith("/sci-fi/")) { if (key === "renee") customPaths.push("/renee-heart", "/renee");
return "scifi"; 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) // 2. Dynamische Domain-basierte Routen (für Live-Domains)
if (hostname.includes("annieslone.de")) { for (const key of profileKeys) {
return "erotica"; const p = authorData?.[key];
} const customDomains: string[] = p?.customDomain
if (hostname.includes("clarafinch.de")) { ? p.customDomain.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
return "clara"; : [];
}
if (hostname.includes("reneeheart.de") || hostname.includes("renee-heart.de")) { // Statische Standard-Fallbacks
return "renee"; if (key === "erotica") customDomains.push("annieslone.de", "marcvelvet.de");
} if (key === "clara") customDomains.push("clarafinch.de", "clara-finch.de");
if (hostname.includes("hesse-sf.de")) { if (key === "renee") customDomains.push("reneeheart.de", "renee-heart.de");
return "scifi"; 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) // Standard-Fallback (Daniel Hesse SciFi)

View file

@ -3,7 +3,7 @@ import { AuthorData, AuthorProfile, Book, Project } from "../types";
import { import {
Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles, 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 Palette, Type, Image as ImageIcon, Globe
} from "lucide-react"; } from "lucide-react";
import ImagePicker from "./ImagePicker"; import ImagePicker from "./ImagePicker";
@ -594,6 +594,43 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/> />
</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 */} {/* Custom Highlight Section */}
<div className="md:col-span-2 border-t border-slate-800/80 pt-6 mt-2 space-y-4"> <div className="md:col-span-2 border-t border-slate-800/80 pt-6 mt-2 space-y-4">
<div> <div>
@ -616,7 +653,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/> />
</div> </div>
<div className="space-y-1 md:col-span-2"> <div className="space-y-1 md:col-span-2">
<label className="text-xs font-mono text-slate-400 uppercase">Abschnitts-Inhalt (Text)</label> <label className="text-xs font-mono text-slate-400 uppercase">Abschnitts-Inhalt (Markdown & Links unterstützt)</label>
<textarea <textarea
rows={4} rows={4}
value={profile.customSectionContent || ""} value={profile.customSectionContent || ""}
@ -1052,7 +1089,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/> />
</div> </div>
<div className="space-y-1 md:col-span-2"> <div className="space-y-1 md:col-span-2">
<label className="text-xs font-mono text-slate-400 uppercase">Kurzbeschreibung / Vorschau</label> <label className="text-xs font-mono text-slate-400 uppercase">Kurzbeschreibung / Vorschau (Markdown & Links unterstützt)</label>
<textarea <textarea
rows={3} rows={3}
value={projectForm.description || ""} value={projectForm.description || ""}

View file

@ -165,9 +165,23 @@ export default function ClaraPortfolio({ data }: ClaraPortfolioProps) {
<h3 className="text-lg font-serif text-stone-100 group-hover:text-stone-300 transition-colors"> <h3 className="text-lg font-serif text-stone-100 group-hover:text-stone-300 transition-colors">
{project.title} {project.title}
</h3> </h3>
<p className="text-stone-300 text-sm italic line-clamp-3"> <div className="text-stone-300 text-sm italic line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
{project.description} <Markdown
</p> components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-[#8FA797] hover:text-stone-200 font-medium"
onClick={(e) => e.stopPropagation()}
{...props}
/>
)
}}
>
{project.description}
</Markdown>
</div>
<p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 italic" style={{ color: accentColor }}> <p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 italic" style={{ color: accentColor }}>
Details einsehen <ArrowRight className="w-3 h-3" /> Details einsehen <ArrowRight className="w-3 h-3" />
</p> </p>
@ -230,8 +244,21 @@ export default function ClaraPortfolio({ data }: ClaraPortfolioProps) {
</h2> </h2>
)} )}
{data.customSectionContent && ( {data.customSectionContent && (
<div className="text-stone-300 text-sm md:text-base leading-relaxed whitespace-pre-wrap italic"> <div className="text-stone-300 text-sm md:text-base leading-relaxed italic markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
{data.customSectionContent} <Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-[#8FA797] hover:text-stone-200 font-medium"
{...props}
/>
)
}}
>
{data.customSectionContent}
</Markdown>
</div> </div>
)} )}
</section> </section>

View file

@ -165,9 +165,23 @@ export default function EroticaPortfolio({ data }: EroticaPortfolioProps) {
<h3 className="text-lg font-serif text-amber-100 group-hover:text-orange-400 transition-colors"> <h3 className="text-lg font-serif text-amber-100 group-hover:text-orange-400 transition-colors">
{project.title} {project.title}
</h3> </h3>
<p className="text-neutral-300 text-sm italic line-clamp-3"> <div className="text-neutral-300 text-sm italic line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
{project.description} <Markdown
</p> components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-orange-400 hover:text-amber-300 font-medium"
onClick={(e) => e.stopPropagation()}
{...props}
/>
)
}}
>
{project.description}
</Markdown>
</div>
<p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 italic" style={{ color: accentColor }}> <p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 italic" style={{ color: accentColor }}>
Details einsehen <ArrowRight className="w-3 h-3" /> Details einsehen <ArrowRight className="w-3 h-3" />
</p> </p>
@ -230,8 +244,21 @@ export default function EroticaPortfolio({ data }: EroticaPortfolioProps) {
</h2> </h2>
)} )}
{data.customSectionContent && ( {data.customSectionContent && (
<div className="text-neutral-300 text-sm md:text-base leading-relaxed whitespace-pre-wrap italic"> <div className="text-neutral-300 text-sm md:text-base leading-relaxed italic markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
{data.customSectionContent} <Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-orange-400 hover:text-amber-300 font-medium"
{...props}
/>
)
}}
>
{data.customSectionContent}
</Markdown>
</div> </div>
)} )}
</section> </section>

View file

@ -1,4 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Markdown from "react-markdown";
import { AuthorProfile, Book, Project } from "../types"; import { AuthorProfile, Book, Project } from "../types";
import { import {
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music
@ -165,9 +166,23 @@ export default function ReneePortfolio({ data }: ReneePortfolioProps) {
<h3 className="text-lg font-bold text-white group-hover:text-yellow-300 transition-colors"> <h3 className="text-lg font-bold text-white group-hover:text-yellow-300 transition-colors">
{project.title} {project.title}
</h3> </h3>
<p className="text-slate-300 text-sm line-clamp-3"> <div className="text-slate-300 text-sm line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
{project.description} <Markdown
</p> components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-yellow-400 hover:text-pink-400 font-medium"
onClick={(e) => e.stopPropagation()}
{...props}
/>
)
}}
>
{project.description}
</Markdown>
</div>
<p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 font-semibold" style={{ color: secondaryColor }}> <p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 font-semibold" style={{ color: secondaryColor }}>
Details einsehen <ArrowRight className="w-3 h-3" /> Details einsehen <ArrowRight className="w-3 h-3" />
</p> </p>
@ -232,8 +247,21 @@ export default function ReneePortfolio({ data }: ReneePortfolioProps) {
</h2> </h2>
)} )}
{data.customSectionContent && ( {data.customSectionContent && (
<div className="text-slate-300 text-sm md:text-base leading-relaxed whitespace-pre-wrap"> <div className="text-slate-300 text-sm md:text-base leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
{data.customSectionContent} <Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-yellow-400 hover:text-pink-400 font-medium"
{...props}
/>
)
}}
>
{data.customSectionContent}
</Markdown>
</div> </div>
)} )}
</section> </section>
@ -283,9 +311,22 @@ export default function ReneePortfolio({ data }: ReneePortfolioProps) {
</div> </div>
)} )}
<p className="text-slate-300 text-sm leading-relaxed whitespace-pre-wrap"> <div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
{selectedBook.description} <Markdown
</p> components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-yellow-400 hover:text-pink-400 font-medium"
{...props}
/>
)
}}
>
{selectedBook.description}
</Markdown>
</div>
{/* Spotify Playlist player if configured */} {/* Spotify Playlist player if configured */}
{selectedBook.spotifyPlaylistId && ( {selectedBook.spotifyPlaylistId && (
@ -359,9 +400,22 @@ export default function ReneePortfolio({ data }: ReneePortfolioProps) {
/> />
</div> </div>
<p className="text-slate-300 text-sm leading-relaxed whitespace-pre-wrap"> <div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
{selectedProject.description} <Markdown
</p> components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-yellow-400 hover:text-pink-400 font-medium"
{...props}
/>
)
}}
>
{selectedProject.description || "Für dieses Schreibprojekt wurden noch keine weiteren Details abgelegt."}
</Markdown>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -181,9 +181,23 @@ export default function SciFiPortfolio({ data }: SciFiPortfolioProps) {
<h3 className="text-xl font-bold text-white group-hover:text-cyan-300 transition-colors"> <h3 className="text-xl font-bold text-white group-hover:text-cyan-300 transition-colors">
{project.title} {project.title}
</h3> </h3>
<p className="text-slate-300 text-sm leading-relaxed line-clamp-3"> <div className="text-slate-300 text-sm leading-relaxed line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
{project.description} <Markdown
</p> components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
onClick={(e) => e.stopPropagation()}
{...props}
/>
)
}}
>
{project.description}
</Markdown>
</div>
<p className="font-mono text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1" style={{ color: accentColor }}> <p className="font-mono text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1" style={{ color: accentColor }}>
Details anzeigen <ArrowRight className="w-3 h-3" /> Details anzeigen <ArrowRight className="w-3 h-3" />
</p> </p>
@ -250,8 +264,21 @@ export default function SciFiPortfolio({ data }: SciFiPortfolioProps) {
</h2> </h2>
)} )}
{data.customSectionContent && ( {data.customSectionContent && (
<div className="text-slate-300 text-sm leading-relaxed whitespace-pre-wrap"> <div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
{data.customSectionContent} <Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
{...props}
/>
)
}}
>
{data.customSectionContent}
</Markdown>
</div> </div>
)} )}
</section> </section>

View file

@ -56,6 +56,8 @@ export const defaultAuthorData: AuthorData = {
booksTitle: "Bücherregal", booksTitle: "Bücherregal",
booksSubtitle: "Entdecken Sie meine verlegten Romane und E-Books mit Hörbeispielen.", booksSubtitle: "Entdecken Sie meine verlegten Romane und E-Books mit Hörbeispielen.",
footerText: "Dokumentation, Fragmente und stellare Chiffren. Verschlüsselt mit moderner Node.JS Technologie.", footerText: "Dokumentation, Fragmente und stellare Chiffren. Verschlüsselt mit moderner Node.JS Technologie.",
customDomain: "hesse-sf.de, danielhesse.de",
customPath: "/sci-fi, /daniel-hesse",
accentColor: "#06b6d4", accentColor: "#06b6d4",
secondaryColor: "#a855f7", secondaryColor: "#a855f7",
backgroundColor: "#020617", backgroundColor: "#020617",
@ -118,6 +120,8 @@ export const defaultAuthorData: AuthorData = {
booksTitle: "Das Samtregal", booksTitle: "Das Samtregal",
booksSubtitle: "Verführerische Geschichten und sinnliche Romane für besondere Stunden.", booksSubtitle: "Verführerische Geschichten und sinnliche Romane für besondere Stunden.",
footerText: "Mit Liebe und Leidenschaft geschrieben. Alle Werke geschützt.", footerText: "Mit Liebe und Leidenschaft geschrieben. Alle Werke geschützt.",
customDomain: "annieslone.de, marcvelvet.de",
customPath: "/sensual-moments, /annie-slone, /marc-velvet",
accentColor: "#f97316", accentColor: "#f97316",
secondaryColor: "#f59e0b", secondaryColor: "#f59e0b",
backgroundColor: "#0a0a0a", backgroundColor: "#0a0a0a",
@ -180,6 +184,8 @@ export const defaultAuthorData: AuthorData = {
booksTitle: "Veröffentlichte Bände", booksTitle: "Veröffentlichte Bände",
booksSubtitle: "Lyrikbände und Naturprosa zum Verweilen und Innehalten.", booksSubtitle: "Lyrikbände und Naturprosa zum Verweilen und Innehalten.",
footerText: "Gedruckt auf virtuellem Pergament. Stille Notizen aus Wald und Flur.", footerText: "Gedruckt auf virtuellem Pergament. Stille Notizen aus Wald und Flur.",
customDomain: "clarafinch.de, clara-finch.de",
customPath: "/clara-finch, /clara",
accentColor: "#10b981", accentColor: "#10b981",
secondaryColor: "#84cc16", secondaryColor: "#84cc16",
backgroundColor: "#064e3b", backgroundColor: "#064e3b",
@ -242,6 +248,8 @@ export const defaultAuthorData: AuthorData = {
booksTitle: "Gefühlvolle Romane", booksTitle: "Gefühlvolle Romane",
booksSubtitle: "Geschichten zum Mitfühlen, Träumen und Genießen.", booksSubtitle: "Geschichten zum Mitfühlen, Träumen und Genießen.",
footerText: "Geschrieben mit viel Gefühl und Herzblut.", footerText: "Geschrieben mit viel Gefühl und Herzblut.",
customDomain: "reneeheart.de, renee-heart.de",
customPath: "/renee-heart, /renee",
accentColor: "#eab308", accentColor: "#eab308",
secondaryColor: "#ec4899", secondaryColor: "#ec4899",
backgroundColor: "#0f172a", backgroundColor: "#0f172a",

View file

@ -29,6 +29,10 @@ export interface AuthorProfile {
customSectionTitle?: string; customSectionTitle?: string;
customSectionContent?: string; customSectionContent?: string;
// Domain & Path Routing
customDomain?: string; // e.g., "annieslone.de, annie-slone.de"
customPath?: string; // e.g., "/sensual-moments, /annie"
// Custom static texts // Custom static texts
badgeText?: string; badgeText?: string;
aboutTitle?: string; aboutTitle?: string;