Files
songs2yt/components/MobileSidebar.tsx
T
2026-07-17 02:23:59 +02:00

170 lines
5.2 KiB
TypeScript

"use client";
import { Children, useEffect, useState, type ReactNode } from "react";
type Props = {
open: boolean;
onClose: () => void;
title?: string;
children: ReactNode;
};
const PANEL_MS = 420;
const ITEM_BASE_MS = 90;
function CloseIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M6 6l12 12M18 6L6 18" strokeLinecap="round" />
</svg>
);
}
export function MobileSidebar({ open, onClose, title = "Menu", children }: Props) {
const [mounted, setMounted] = useState(false);
const [visible, setVisible] = useState(false);
useEffect(() => {
if (open) {
setMounted(true);
const frame = requestAnimationFrame(() => setVisible(true));
return () => cancelAnimationFrame(frame);
}
setVisible(false);
const timer = window.setTimeout(() => setMounted(false), PANEL_MS);
return () => window.clearTimeout(timer);
}, [open]);
useEffect(() => {
if (!mounted) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.body.style.overflow = "hidden";
window.addEventListener("keydown", onKeyDown);
return () => {
document.body.style.overflow = "";
window.removeEventListener("keydown", onKeyDown);
};
}, [mounted, onClose]);
if (!mounted) return null;
return (
<div className="fixed inset-0 z-50 sm:hidden">
<button
type="button"
aria-label="Close menu"
className={`absolute inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-300 ease-out ${
visible ? "opacity-100" : "opacity-0"
}`}
onClick={onClose}
/>
<button
type="button"
onClick={onClose}
aria-label="Close menu"
style={{ animationDelay: visible ? "120ms" : "0ms" }}
className={`fixed right-4 top-4 z-[60] flex h-10 w-10 items-center justify-center rounded-full border border-gray-700 bg-surface text-gray-300 shadow-lg transition-shadow hover:bg-surface-light hover:text-white ${
visible ? "mobile-sidebar-close-btn-open" : "mobile-sidebar-close-btn-close"
}`}
>
<CloseIcon className="h-5 w-5" />
</button>
<aside
className={`absolute right-0 top-0 flex h-full w-72 max-w-[85vw] flex-col border-l border-gray-800 bg-surface shadow-2xl ${
visible ? "mobile-sidebar-panel-open" : "mobile-sidebar-panel-close"
}`}
>
<div
className={`border-b border-gray-800 px-5 py-5 transition-all duration-300 ease-out ${
visible ? "translate-y-0 opacity-100" : "-translate-y-2 opacity-0"
}`}
style={{ transitionDelay: visible ? "100ms" : "0ms" }}
>
<span className="text-sm font-semibold uppercase tracking-wider text-gray-400">
{title}
</span>
</div>
<nav className="flex flex-1 flex-col gap-1 overflow-y-auto p-4">
{Children.map(children, (child, index) => (
<div
key={index}
className={`transition-all duration-300 ease-out ${
visible ? "translate-x-0 opacity-100" : "translate-x-8 opacity-0"
}`}
style={{
transitionDelay: visible ? `${ITEM_BASE_MS + index * 50}ms` : "0ms",
}}
>
{child}
</div>
))}
</nav>
</aside>
</div>
);
}
export function MobileMenuButton({
onClick,
open = false,
}: {
onClick: () => void;
open?: boolean;
}) {
return (
<button
type="button"
onClick={onClick}
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
className="relative rounded p-2 text-gray-300 transition-all duration-300 hover:scale-105 hover:bg-white/10 hover:text-white active:scale-95 sm:hidden"
>
<span className="relative block h-6 w-6">
<span
className={`absolute left-0 top-[5px] block h-0.5 w-6 rounded-full bg-current transition-all duration-300 ease-out ${
open ? "top-[11px] rotate-45" : ""
}`}
/>
<span
className={`absolute left-0 top-[11px] block h-0.5 w-6 rounded-full bg-current transition-all duration-300 ease-out ${
open ? "scale-x-0 opacity-0" : ""
}`}
/>
<span
className={`absolute left-0 top-[17px] block h-0.5 w-6 rounded-full bg-current transition-all duration-300 ease-out ${
open ? "top-[11px] -rotate-45" : ""
}`}
/>
</span>
</button>
);
}
export function sidebarLinkClass(active = false) {
return `block rounded-lg px-4 py-3 text-base font-medium transition-all duration-200 hover:translate-x-0.5 ${
active ? "bg-surface-light text-white" : "text-gray-300 hover:bg-surface-light hover:text-white"
}`;
}
export function dashboardSidebarLinkClass(active = false, danger = false) {
const classes = ["mobile-sidebar-link"];
if (active) classes.push("mobile-sidebar-link-active");
if (danger) classes.push("mobile-sidebar-link-danger");
return classes.join(" ");
}