webstack-author/src/App.tsx
2026-08-15 10:21:35 +02:00

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>
);
}