Aktueller Stand
This commit is contained in:
@@ -11,10 +11,15 @@ export default function NavBar() {
|
||||
const isAdmin = data?.user?.role === "ADMIN" || data?.user?.role === "SUPERADMIN";
|
||||
const isSuperAdmin = data?.user?.role === "SUPERADMIN";
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [logoBrightness, setLogoBrightness] = useState<number | null>(null);
|
||||
const [isDarkTheme, setIsDarkTheme] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [appName, setAppName] = useState("Vereinskalender");
|
||||
const linkClass = (href: string) =>
|
||||
pathname === href
|
||||
? "rounded-full bg-slate-900 px-3 py-1 text-white"
|
||||
: "rounded-full px-3 py-1 text-slate-700 hover:bg-slate-100";
|
||||
? "nav-link-active rounded-full px-3 py-1"
|
||||
: "nav-link rounded-full px-3 py-1";
|
||||
|
||||
useEffect(() => {
|
||||
const loadLogo = async () => {
|
||||
@@ -35,21 +40,113 @@ export default function NavBar() {
|
||||
loadLogo();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAppName = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/settings/app-name");
|
||||
if (!response.ok) return;
|
||||
const payload = await response.json();
|
||||
setAppName(payload.name || "Vereinskalender");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
loadAppName();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
const root = document.documentElement;
|
||||
const updateTheme = () => {
|
||||
setIsDarkTheme(root.dataset.theme === "dark");
|
||||
};
|
||||
updateTheme();
|
||||
const observer = new MutationObserver(updateTheme);
|
||||
observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => {
|
||||
setIsScrolled(window.scrollY > 12);
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logoUrl) return;
|
||||
let cancelled = false;
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.src = logoUrl;
|
||||
img.onload = () => {
|
||||
if (cancelled) return;
|
||||
const canvas = document.createElement("canvas");
|
||||
const size = 32;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(img, 0, 0, size, size);
|
||||
const data = ctx.getImageData(0, 0, size, size).data;
|
||||
let total = 0;
|
||||
let count = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const alpha = data[i + 3];
|
||||
if (alpha === 0) continue;
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
total += 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
count += 1;
|
||||
}
|
||||
if (count > 0) {
|
||||
setLogoBrightness(total / count);
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [logoUrl]);
|
||||
|
||||
const shouldInvertLogo =
|
||||
logoBrightness !== null &&
|
||||
((isDarkTheme && logoBrightness > 140) ||
|
||||
(!isDarkTheme && logoBrightness < 200));
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-20 border-b border-slate-200/70 bg-white/70 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
|
||||
<Link href="/" className="flex items-center gap-3 text-lg font-semibold tracking-tight text-slate-900">
|
||||
<header className="sticky top-0 z-20 border-b border-slate-200/70 bg-white/70 backdrop-blur transition-all duration-300">
|
||||
<div
|
||||
className={`mx-auto flex max-w-6xl items-center justify-between px-4 transition-all duration-300 ${
|
||||
isScrolled ? "py-2" : "py-4"
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className={`brand-title flex items-center gap-3 font-semibold tracking-tight text-slate-900 transition-all duration-300 ${
|
||||
isScrolled ? "text-base" : "text-lg"
|
||||
}`}
|
||||
>
|
||||
{logoUrl && (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt="Vereinskalender Logo"
|
||||
className="h-8 w-auto max-w-[140px] object-contain"
|
||||
className={`w-auto object-contain transition-all duration-300 ${
|
||||
isScrolled ? "h-7 max-w-[140px]" : "h-12 max-w-[210px]"
|
||||
}`}
|
||||
style={shouldInvertLogo ? { filter: "invert(1)" } : undefined}
|
||||
onError={() => setLogoUrl(null)}
|
||||
/>
|
||||
)}
|
||||
<span>Vereinskalender</span>
|
||||
<span>{appName}</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-3 text-sm">
|
||||
<nav className="hidden items-center gap-3 text-sm md:flex">
|
||||
{data?.user && (
|
||||
<>
|
||||
{isAdmin && (
|
||||
@@ -71,8 +168,18 @@ export default function NavBar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => signOut()}
|
||||
className="btn-primary"
|
||||
className="btn-primary inline-flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M10 6h8a2 2 0 012 2v8a2 2 0 01-2 2h-8" strokeLinecap="round" />
|
||||
<path d="M14 12H4m0 0l3-3M4 12l3 3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
Logout
|
||||
</button>
|
||||
) : (
|
||||
@@ -85,6 +192,99 @@ export default function NavBar() {
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-slate-200 p-2 text-slate-600 md:hidden"
|
||||
onClick={() => setMobileOpen((prev) => !prev)}
|
||||
aria-label={mobileOpen ? "Menü schließen" : "Menü öffnen"}
|
||||
aria-expanded={mobileOpen}
|
||||
>
|
||||
<span className="relative block h-5 w-5">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={`absolute inset-0 h-5 w-5 transition-all duration-300 ${
|
||||
mobileOpen ? "rotate-90 opacity-0" : "rotate-0 opacity-100"
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M3 6h18M3 12h18M3 18h18" strokeLinecap="round" />
|
||||
</svg>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={`absolute inset-0 h-5 w-5 transition-all duration-300 ${
|
||||
mobileOpen ? "rotate-0 opacity-100" : "-rotate-90 opacity-0"
|
||||
}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M6 6l12 12M18 6l-12 12" strokeLinecap="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-10 bg-black/30 md:hidden"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`relative z-20 overflow-hidden transition-all duration-300 md:hidden ${
|
||||
mobileOpen ? "max-h-96 opacity-100" : "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-6xl space-y-2 px-4 pb-4">
|
||||
{data?.user && (
|
||||
<>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Link href="/admin" className="nav-link block rounded-xl px-3 py-2 text-sm">
|
||||
Admin
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/users"
|
||||
className="nav-link block rounded-xl px-3 py-2 text-sm"
|
||||
>
|
||||
Registrierungen
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
<Link href="/settings" className="nav-link block rounded-xl px-3 py-2 text-sm">
|
||||
Einstellungen
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{data?.user ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => signOut()}
|
||||
className="btn-primary inline-flex w-full items-center justify-center gap-2"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M10 6h8a2 2 0 012 2v8a2 2 0 01-2 2h-8" strokeLinecap="round" />
|
||||
<path d="M14 12H4m0 0l3-3M4 12l3 3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
Logout
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => signIn()}
|
||||
className="btn-accent w-full"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user