263 lines
11 KiB
TypeScript
263 lines
11 KiB
TypeScript
|
|
"use client";
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
ArrowRightStartOnRectangleIcon,
|
|||
|
|
CalendarDaysIcon,
|
|||
|
|
ChartBarIcon,
|
|||
|
|
BellAlertIcon,
|
|||
|
|
BoltIcon,
|
|||
|
|
ChevronRightIcon,
|
|||
|
|
ChevronUpDownIcon,
|
|||
|
|
Cog6ToothIcon,
|
|||
|
|
DevicePhoneMobileIcon,
|
|||
|
|
FireIcon,
|
|||
|
|
MoonIcon,
|
|||
|
|
ScaleIcon,
|
|||
|
|
ShoppingCartIcon,
|
|||
|
|
SparklesIcon,
|
|||
|
|
SunIcon,
|
|||
|
|
UserIcon,
|
|||
|
|
UsersIcon,
|
|||
|
|
} from "@heroicons/react/24/outline";
|
|||
|
|
import Link from "next/link";
|
|||
|
|
import { usePathname, useRouter } from "next/navigation";
|
|||
|
|
import { useEffect, useRef, useState } from "react";
|
|||
|
|
import { signOut, useSession } from "@/lib/auth-client";
|
|||
|
|
import { useTheme } from "./theme-provider";
|
|||
|
|
|
|||
|
|
type NavItem = {
|
|||
|
|
href?: string;
|
|||
|
|
label: string;
|
|||
|
|
icon: React.ComponentType<{ className?: string }>;
|
|||
|
|
disabled?: boolean;
|
|||
|
|
hint?: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type NavGroup = {
|
|||
|
|
label: string;
|
|||
|
|
items: NavItem[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const navGroups: NavGroup[] = [
|
|||
|
|
{
|
|||
|
|
label: "Heute",
|
|||
|
|
items: [
|
|||
|
|
{ href: "/", label: "Tagesansicht", icon: CalendarDaysIcon },
|
|||
|
|
{ href: "/wochenbudget", label: "Wochenbudget", icon: ChartBarIcon },
|
|||
|
|
{ href: "/statistiken", label: "Statistiken", icon: ChartBarIcon },
|
|||
|
|
{ href: "/erinnerungen", label: "Erinnerungen", icon: BellAlertIcon },
|
|||
|
|
{ href: "/aktivitaeten", label: "Aktivitäten", icon: BoltIcon },
|
|||
|
|
{ href: "/wearables", label: "Wearables", icon: DevicePhoneMobileIcon },
|
|||
|
|
{ href: "/hellfireclub", label: "Hellfireclub", icon: FireIcon },
|
|||
|
|
{ href: "/gewicht", label: "Gewicht", icon: ScaleIcon },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: "Ernährung",
|
|||
|
|
items: [
|
|||
|
|
{ href: "/planung", label: "Essensplanung", icon: SparklesIcon },
|
|||
|
|
{ href: "/lebensmittel", label: "Lebensmittel", icon: ShoppingCartIcon },
|
|||
|
|
{ href: "/maerkte", label: "Märkte", icon: ShoppingCartIcon },
|
|||
|
|
{ href: "/rezepte", label: "Rezepte", icon: SparklesIcon },
|
|||
|
|
{ href: "/rezepte/pruefung", label: "Rezeptprüfung", icon: SparklesIcon },
|
|||
|
|
{ href: "/einkaufsliste", label: "Einkaufsliste", icon: ShoppingCartIcon },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
label: "System",
|
|||
|
|
items: [
|
|||
|
|
{ href: "/benutzer", label: "Benutzer", icon: UsersIcon },
|
|||
|
|
{ href: "/aktivitaetsfeed", label: "Activity Feed", icon: BellAlertIcon },
|
|||
|
|
{ href: "/einstellungen", label: "Einstellungen", icon: Cog6ToothIcon },
|
|||
|
|
{ href: "/einstellungen/punkte", label: "Punktevergabe", icon: ChartBarIcon },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export function Sidebar() {
|
|||
|
|
const pathname = usePathname();
|
|||
|
|
const router = useRouter();
|
|||
|
|
const { data: session } = useSession();
|
|||
|
|
const { isDark, toggleDark } = useTheme();
|
|||
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|||
|
|
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
|||
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
function handleOutside(e: MouseEvent) {
|
|||
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|||
|
|
setMenuOpen(false);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (menuOpen) document.addEventListener("mousedown", handleOutside);
|
|||
|
|
return () => document.removeEventListener("mousedown", handleOutside);
|
|||
|
|
}, [menuOpen]);
|
|||
|
|
|
|||
|
|
function toggleGroup(label: string) {
|
|||
|
|
setCollapsedGroups((prev) => {
|
|||
|
|
const next = new Set(prev);
|
|||
|
|
if (next.has(label)) next.delete(label);
|
|||
|
|
else next.add(label);
|
|||
|
|
return next;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isActive = (href: string) => {
|
|||
|
|
if (href === "/") return pathname === "/";
|
|||
|
|
return pathname === href || pathname.startsWith(`${href}/`);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
async function handleSignOut() {
|
|||
|
|
await signOut();
|
|||
|
|
if (session?.user?.id) {
|
|||
|
|
localStorage.removeItem(`hellth-theme:${session.user.id}`);
|
|||
|
|
}
|
|||
|
|
document.documentElement.classList.remove("dark");
|
|||
|
|
router.push("/login");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<aside className="sticky top-0 hidden h-screen w-64 shrink-0 flex-col border-r border-gray-200 bg-white dark:border-neutral-800 dark:bg-neutral-900 lg:flex">
|
|||
|
|
<div className="border-b border-gray-100 px-5 py-5 dark:border-neutral-800">
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<div className="flex h-[36.8px] w-[36.8px] items-center justify-center rounded-lg bg-[#0F766E] text-xs font-bold text-white">
|
|||
|
|
HH
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p className="leading-none text-sm font-semibold text-gray-900 dark:text-neutral-100">Hellth Hub</p>
|
|||
|
|
<p className="mt-0.5 text-xs text-gray-400">Home-Lab Health</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<nav className="flex-1 space-y-6 overflow-y-auto px-3 py-4">
|
|||
|
|
{navGroups.map((group) => (
|
|||
|
|
<div key={group.label}>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => toggleGroup(group.label)}
|
|||
|
|
className="mb-1.5 flex w-full items-center justify-between px-2 text-xs font-semibold uppercase tracking-wider text-gray-400 hover:text-gray-600 dark:hover:text-neutral-300"
|
|||
|
|
>
|
|||
|
|
{group.label}
|
|||
|
|
<ChevronRightIcon className={`h-3 w-3 transition-transform ${!collapsedGroups.has(group.label) ? "rotate-90" : ""}`} />
|
|||
|
|
</button>
|
|||
|
|
{!collapsedGroups.has(group.label) ? (
|
|||
|
|
<ul className="space-y-0.5">
|
|||
|
|
{group.items.map((item) => {
|
|||
|
|
if (item.disabled || !item.href) {
|
|||
|
|
return (
|
|||
|
|
<li key={`${group.label}-${item.label}`}>
|
|||
|
|
<span className="group flex cursor-not-allowed items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-gray-300 dark:text-neutral-600">
|
|||
|
|
<item.icon className="h-4.5 w-4.5 shrink-0" />
|
|||
|
|
<span className="flex-1">{item.label}</span>
|
|||
|
|
{item.hint ? <span className="text-[10px] uppercase">{item.hint}</span> : null}
|
|||
|
|
</span>
|
|||
|
|
</li>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const active = isActive(item.href);
|
|||
|
|
return (
|
|||
|
|
<li key={item.href}>
|
|||
|
|
<Link
|
|||
|
|
href={item.href}
|
|||
|
|
className={`group flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium transition-all ${
|
|||
|
|
active
|
|||
|
|
? "bg-teal-50 text-[#0F766E] dark:bg-teal-950/40"
|
|||
|
|
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-neutral-300 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<item.icon className={`h-4.5 w-4.5 shrink-0 ${active ? "text-[#0F766E]" : "text-gray-400 group-hover:text-gray-600"}`} />
|
|||
|
|
<span className="flex-1">{item.label}</span>
|
|||
|
|
{active ? <ChevronRightIcon className="h-3.5 w-3.5 text-[#0F766E]" /> : null}
|
|||
|
|
</Link>
|
|||
|
|
</li>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</ul>
|
|||
|
|
) : null}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</nav>
|
|||
|
|
|
|||
|
|
<div className="mt-auto border-t border-gray-100 px-3 py-2 dark:border-neutral-800">
|
|||
|
|
<div ref={menuRef} className="relative">
|
|||
|
|
{menuOpen ? (
|
|||
|
|
<div className="absolute bottom-full left-0 right-0 z-50 mb-1 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg dark:border-neutral-800 dark:bg-neutral-900">
|
|||
|
|
<div className="border-b border-gray-100 px-4 py-3 dark:border-neutral-800">
|
|||
|
|
<p className="truncate text-sm font-semibold text-gray-900 dark:text-neutral-100">
|
|||
|
|
{session?.user?.name ?? "Administrator"}
|
|||
|
|
</p>
|
|||
|
|
<p className="truncate text-xs text-gray-400">{session?.user?.email ?? ""}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="border-b border-gray-100 px-4 py-2.5 dark:border-neutral-800">
|
|||
|
|
<div className="flex items-center justify-between">
|
|||
|
|
<div className="flex items-center gap-2.5">
|
|||
|
|
{isDark ? <SunIcon className="h-4 w-4 text-gray-400" /> : <MoonIcon className="h-4 w-4 text-gray-400" />}
|
|||
|
|
<span className="text-sm text-gray-700 dark:text-neutral-300">
|
|||
|
|
{isDark ? "Helles Design" : "Dunkles Design"}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={toggleDark}
|
|||
|
|
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
|
|||
|
|
isDark ? "bg-[#0F766E]" : "bg-gray-200"
|
|||
|
|
}`}
|
|||
|
|
role="switch"
|
|||
|
|
aria-checked={isDark}
|
|||
|
|
aria-label="Dunkles Design umschalten"
|
|||
|
|
>
|
|||
|
|
<span
|
|||
|
|
className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
|
|||
|
|
isDark ? "translate-x-4" : "translate-x-0"
|
|||
|
|
}`}
|
|||
|
|
/>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Link
|
|||
|
|
href="/einstellungen"
|
|||
|
|
onClick={() => setMenuOpen(false)}
|
|||
|
|
className="flex items-center gap-2.5 px-4 py-2.5 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|||
|
|
>
|
|||
|
|
<Cog6ToothIcon className="h-4 w-4 text-gray-400" />
|
|||
|
|
Einstellungen
|
|||
|
|
</Link>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={handleSignOut}
|
|||
|
|
className="flex w-full items-center gap-2.5 px-4 py-2.5 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|||
|
|
>
|
|||
|
|
<ArrowRightStartOnRectangleIcon className="h-4 w-4 text-gray-400" />
|
|||
|
|
Abmelden
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => setMenuOpen((v) => !v)}
|
|||
|
|
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 hover:bg-gray-50 dark:hover:bg-neutral-800"
|
|||
|
|
>
|
|||
|
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#0F766E] text-xs font-bold text-white">
|
|||
|
|
<UserIcon className="h-4 w-4" />
|
|||
|
|
</div>
|
|||
|
|
<div className="min-w-0 flex-1 text-left">
|
|||
|
|
<p className="truncate text-xs font-medium text-gray-900 dark:text-neutral-100">
|
|||
|
|
{session?.user?.name ?? "Administrator"}
|
|||
|
|
</p>
|
|||
|
|
<p className="truncate text-xs text-gray-400">{session?.user?.email ?? ""}</p>
|
|||
|
|
</div>
|
|||
|
|
<ChevronUpDownIcon className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|