mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
262 lines
7.6 KiB
TypeScript
262 lines
7.6 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface NavItem {
|
|
title: string;
|
|
href: string;
|
|
children?: NavItem[];
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{
|
|
title: "Installation",
|
|
href: "/installation",
|
|
},
|
|
{
|
|
title: "Storages",
|
|
href: "/storages",
|
|
children: [
|
|
{ title: "Google Drive", href: "/storages/google-drive" },
|
|
{ title: "Cloudflare R2", href: "/storages/cloudflare-r2" },
|
|
],
|
|
},
|
|
{
|
|
title: "Notifiers",
|
|
href: "/notifiers",
|
|
children: [
|
|
{ title: "Slack", href: "/notifiers/slack" },
|
|
{ title: "Microsoft Teams", href: "/notifiers/teams" },
|
|
],
|
|
},
|
|
{
|
|
title: "Reset password",
|
|
href: "/password",
|
|
},
|
|
];
|
|
|
|
export default function DocsSidebarComponent() {
|
|
const pathname = usePathname();
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [manuallyToggledSections, setManuallyToggledSections] = useState<
|
|
Set<string>
|
|
>(new Set());
|
|
|
|
const isActive = (href: string) => {
|
|
return pathname === href;
|
|
};
|
|
|
|
const isParentActive = (item: NavItem) => {
|
|
if (item.children) {
|
|
return item.children.some((child) => pathname === child.href);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Determine if a section should be expanded
|
|
const isSectionExpanded = (href: string) => {
|
|
// If manually toggled, respect that
|
|
if (manuallyToggledSections.has(href)) {
|
|
return true;
|
|
}
|
|
// Auto-expand if a child page is active
|
|
const item = navItems.find((i) => i.href === href);
|
|
if (item && isParentActive(item)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Manage body overflow when mobile menu is open
|
|
useEffect(() => {
|
|
if (isMobileMenuOpen) {
|
|
document.body.style.overflow = "hidden";
|
|
} else {
|
|
document.body.style.overflow = "";
|
|
}
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
document.body.style.overflow = "";
|
|
};
|
|
}, [isMobileMenuOpen]);
|
|
|
|
const toggleSection = (href: string, hasChildren: boolean) => {
|
|
if (!hasChildren) return;
|
|
|
|
setManuallyToggledSections((prev) => {
|
|
const newSet = new Set(prev);
|
|
if (newSet.has(href)) {
|
|
newSet.delete(href);
|
|
} else {
|
|
newSet.add(href);
|
|
}
|
|
return newSet;
|
|
});
|
|
};
|
|
|
|
const renderSidebarContent = () => (
|
|
<nav className="space-y-1">
|
|
{navItems.map((item) => (
|
|
<div key={item.href}>
|
|
<div className="flex items-center">
|
|
<Link
|
|
href={item.href}
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
className={`flex-1 rounded-lg px-3 py-2 text-sm font-medium transition-colors ${
|
|
isActive(item.href)
|
|
? "bg-blue-50 text-blue-700"
|
|
: "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
|
|
}`}
|
|
>
|
|
{item.title}
|
|
</Link>
|
|
{item.children && (
|
|
<button
|
|
onClick={() => toggleSection(item.href, !!item.children)}
|
|
className={`ml-1 rounded-lg p-2 transition-all duration-200 ${
|
|
isActive(item.href)
|
|
? "text-blue-700 hover:bg-blue-100"
|
|
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900"
|
|
}`}
|
|
aria-label={`Toggle ${item.title} section`}
|
|
>
|
|
<svg
|
|
className={`h-4 w-4 transition-transform duration-200 ${
|
|
isSectionExpanded(item.href) ? "rotate-180" : ""
|
|
}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 9l-7 7-7-7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
{item.children && (
|
|
<div
|
|
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
|
isSectionExpanded(item.href)
|
|
? "max-h-96 opacity-100"
|
|
: "max-h-0 opacity-0"
|
|
}`}
|
|
>
|
|
<div className="ml-4 mt-1 space-y-1 border-l-2 border-gray-200 pl-3">
|
|
{item.children.map((child) => (
|
|
<Link
|
|
key={child.href}
|
|
href={child.href}
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
className={`block rounded-lg px-3 py-2 text-sm transition-colors ${
|
|
isActive(child.href)
|
|
? "bg-blue-50 text-blue-700 font-medium"
|
|
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
}`}
|
|
>
|
|
{child.title}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</nav>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile Menu Button */}
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
className="fixed bottom-4 right-4 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg hover:bg-blue-700 lg:hidden"
|
|
aria-label="Toggle navigation menu"
|
|
>
|
|
{isMobileMenuOpen ? (
|
|
<svg
|
|
className="h-6 w-6"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
className="h-6 w-6"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M4 6h16M4 12h16M4 18h16"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Mobile Menu Overlay */}
|
|
{isMobileMenuOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 backdrop-blur-md bg-white/10 lg:hidden"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Mobile Menu */}
|
|
<aside
|
|
className={`fixed bottom-0 left-0 right-0 z-40 max-h-[80vh] overflow-y-auto rounded-t-2xl border-t border-gray-200 bg-white p-6 shadow-2xl transition-transform duration-300 lg:hidden ${
|
|
isMobileMenuOpen ? "translate-y-0" : "translate-y-full"
|
|
}`}
|
|
>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold text-gray-900">Navigation</h2>
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100"
|
|
aria-label="Close menu"
|
|
>
|
|
<svg
|
|
className="h-5 w-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{renderSidebarContent()}
|
|
</aside>
|
|
|
|
{/* Desktop Sidebar */}
|
|
<aside className="hidden w-64 border-r border-gray-200 bg-white lg:block">
|
|
<div className="sticky top-0 h-screen overflow-y-auto p-6">
|
|
{renderSidebarContent()}
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|