189 lines
7.2 KiB
TypeScript
189 lines
7.2 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
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";
|
|
|
|
export default function App() {
|
|
const [authorData, setAuthorData] = useState<AuthorData | null>(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);
|
|
});
|
|
}, []);
|
|
|
|
// Ermittelt die aktive Suite basierend auf Pfad-Überschreibungen oder dem Domainnamen (Hostname)
|
|
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 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. 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)
|
|
return "scifi";
|
|
};
|
|
|
|
// Dynamisch den Dokumententitel (Browsertab-Titel) basierend auf Suite und Autorendaten anpassen
|
|
useEffect(() => {
|
|
if (!authorData) {
|
|
document.title = "Lade Autoren-Portfolio...";
|
|
return;
|
|
}
|
|
if (currentPath === "/admin" || currentPath.startsWith("/admin/")) {
|
|
document.title = "Admin-Zentrale | Autor-Suiten";
|
|
return;
|
|
}
|
|
|
|
const activeSuite = getActiveSuite();
|
|
if (activeSuite === "erotica") {
|
|
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";
|
|
}
|
|
}, [currentPath, authorData]);
|
|
|
|
const handleAdminLogout = () => {
|
|
// Relocate to landing site after administration work
|
|
window.history.pushState({}, "", "/");
|
|
setCurrentPath("/");
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col items-center justify-center space-y-4">
|
|
<RefreshCw className="w-8 h-8 animate-spin text-cyan-400" />
|
|
<p className="font-mono text-xs text-slate-500 uppercase tracking-widest">Kryptografisches Terminal bootet...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!authorData) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col items-center justify-center p-4">
|
|
<div className="max-w-md p-6 bg-slate-900 border border-slate-800 rounded-xl text-center space-y-3">
|
|
<h2 className="text-lg font-bold text-rose-450 text-rose-400">Verbindungsfehler</h2>
|
|
<p className="text-xs text-slate-400 leading-relaxed">
|
|
Die Verbindung zum lokalen Datenspeicher-Server konnte nicht hergestellt werden. Bitte vergewissern Sie sich, dass Ihr Express API-Server ordnungsgemäß läuft.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Purely route-based render isolation structure
|
|
// 1. Administration panel
|
|
if (currentPath === "/admin" || currentPath.startsWith("/admin/")) {
|
|
return <AdminPanel onLogout={handleAdminLogout} />;
|
|
}
|
|
|
|
// Render active portfolio
|
|
const renderActivePortfolio = () => {
|
|
const activeSuite = getActiveSuite();
|
|
if (activeSuite === "erotica") {
|
|
return <EroticaPortfolio data={authorData.erotica} />;
|
|
}
|
|
if (activeSuite === "clara") {
|
|
return <ClaraPortfolio data={authorData.clara} />;
|
|
}
|
|
if (activeSuite === "renee") {
|
|
return <ReneePortfolio data={authorData.renee} />;
|
|
}
|
|
return <SciFiPortfolio data={authorData.scifi} />;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{renderActivePortfolio()}
|
|
|
|
{/* Absolute floating administration bypass link hidden on the bottom left (hover to show) */}
|
|
<div className="fixed bottom-3 left-3 opacity-0 hover:opacity-100 focus-within:opacity-100 transition-opacity duration-300 z-40 bg-neutral-950/90 border border-neutral-800 p-2 rounded-xl flex items-center text-xs font-mono shadow-xl backdrop-blur-sm">
|
|
<a
|
|
href="/admin"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
window.history.pushState({}, "", "/admin");
|
|
setCurrentPath("/admin");
|
|
}}
|
|
className="text-stone-500 hover:text-cyan-400 font-bold flex items-center gap-1.5 transition-colors px-1"
|
|
>
|
|
<Lock className="w-3 h-3" />
|
|
<span>Admin-Zentrale</span>
|
|
</a>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|