Files
databasus/app/components/DocsSidebarComponent.tsx
Rostislav Dugin bb9cdc5ffc Reapply "FEATURE (ssr): Migrate to NextJS"
This reverts commit 042e10c49c.
2025-11-09 17:54:13 +03:00

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