117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
import { lazy, Suspense, useEffect, useState } from "react";
|
|
import { Lock, RefreshCw } from "lucide-react";
|
|
import type { PublicAuthorData } from "./types";
|
|
|
|
const PortfolioPage = lazy(() => import("./components/PortfolioPage"));
|
|
const AdminPanel = lazy(() => import("./components/AdminPanel"));
|
|
|
|
function LoadingScreen() {
|
|
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">Portfolio wird geladen...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
const [publicData, setPublicData] = useState<PublicAuthorData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [currentPath, setCurrentPath] = useState(window.location.pathname);
|
|
const isAdmin = currentPath === "/admin" || currentPath.startsWith("/admin/");
|
|
|
|
useEffect(() => {
|
|
const handleLocationChange = () => setCurrentPath(window.location.pathname);
|
|
window.addEventListener("popstate", handleLocationChange);
|
|
return () => window.removeEventListener("popstate", handleLocationChange);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isAdmin) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
setLoading(true);
|
|
fetch("/api/author-data", { signal: controller.signal })
|
|
.then((res) => {
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
return res.json() as Promise<PublicAuthorData>;
|
|
})
|
|
.then((data) => setPublicData(data))
|
|
.catch((error) => {
|
|
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
console.error("Fehler beim Abrufen des Portfolios:", error);
|
|
setPublicData(null);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
|
|
return () => controller.abort();
|
|
}, [isAdmin]);
|
|
|
|
useEffect(() => {
|
|
if (isAdmin) document.title = "Administration";
|
|
else if (publicData) document.title = publicData.profile.seoTitle || publicData.profile.name;
|
|
else document.title = "Portfolio";
|
|
}, [isAdmin, publicData]);
|
|
|
|
const navigateTo = (path: string) => {
|
|
window.history.pushState({}, "", path);
|
|
setCurrentPath(path);
|
|
};
|
|
|
|
if (isAdmin) {
|
|
return (
|
|
<Suspense fallback={<LoadingScreen />}>
|
|
<AdminPanel onLogout={() => navigateTo("/")} />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
if (loading) return <LoadingScreen />;
|
|
|
|
if (!publicData) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex 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-400">Verbindungsfehler</h2>
|
|
<p className="text-xs text-slate-400 leading-relaxed">Das Portfolio konnte nicht geladen werden.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isLocalPreview = ["localhost", "127.0.0.1"].includes(window.location.hostname);
|
|
if (currentPath !== "/" && !isLocalPreview) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100 flex items-center justify-center p-4">
|
|
<div className="max-w-md rounded-2xl border border-slate-800 bg-slate-900 p-8 text-center space-y-4">
|
|
<p className="text-xs font-mono uppercase tracking-widest text-slate-500">404</p>
|
|
<h1 className="text-2xl font-bold">Seite nicht gefunden</h1>
|
|
<p className="text-sm text-slate-400">Die angeforderte Seite ist nicht verfügbar.</p>
|
|
<a href="/" className="inline-flex rounded-xl bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-950">Zur Startseite</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Suspense fallback={<LoadingScreen />}>
|
|
<PortfolioPage data={publicData.profile} legalDocuments={publicData.legalDocuments} theme={publicData.theme} />
|
|
<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={(event) => {
|
|
event.preventDefault();
|
|
navigateTo("/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>Administration</span>
|
|
</a>
|
|
</div>
|
|
</Suspense>
|
|
);
|
|
}
|