Initial commit for new panel

This commit is contained in:
AmirReza Jamali
2025-09-09 11:20:26 +03:30
commit 1d4ccb3575
343 changed files with 20031 additions and 0 deletions
@@ -0,0 +1,159 @@
import { title } from "process";
import * as Icons from "../icons";
import { z } from "zod";
const navSubItemSchema: z.ZodType<any> = z.lazy(() =>
z.object({
title: z.string(),
url: z.string(),
icon: z.any(),
items: z.array(navSubItemSchema).default([]),
}),
);
const navItemSchema = z.object({
label: z.string(),
items: z.array(navSubItemSchema),
});
const navDataSchema = z.array(navItemSchema);
export type NavSubItem = z.infer<typeof navSubItemSchema>;
export type NavData = z.infer<typeof navDataSchema>;
export const NAV_DATA: NavData = [
{
label: "MAIN MENU",
items: [
{
title: "Dashboard",
url: "/",
icon: Icons.HomeIcon,
items: [],
},
{
title: "Admins",
url: "/admins",
icon: Icons.User,
items: [],
},
{
title: "companies",
url: "/companies",
icon: Icons.Building,
items: [],
},
{
title: "Customers",
url: "/customers",
icon: Icons.User,
items: [],
},
{
title: "Tenders",
url: "/tenders",
icon: Icons.Table,
items: [],
},
],
},
// {
// label: "MAIN MENU",
// items: [
// {
// title: "Dashboard",
// icon: Icons.HomeIcon,
// items: [
// {
// title: "eCommerce",
// url: "/",
// },
// ],
// },
// {
// title: "Calendar",
// url: "/calendar",
// icon: Icons.Calendar,
// items: [],
// },
// {
// title: "Profile",
// url: "/profile",
// icon: Icons.User,
// items: [],
// },
// {
// title: "Forms",
// icon: Icons.Alphabet,
// items: [
// {
// title: "Form Elements",
// url: "/forms/form-elements",
// },
// {
// title: "Form Layout",
// url: "/forms/form-layout",
// },
// ],
// },
// {
// title: "Tables",
// url: "/tables",
// icon: Icons.Table,
// items: [
// {
// title: "Tables",
// url: "/tables",
// },
// ],
// },
// {
// title: "Pages",
// icon: Icons.Alphabet,
// items: [
// {
// title: "Settings",
// url: "/pages/settings",
// },
// ],
// },
// ],
// },
// {
// label: "OTHERS",
// items: [
// {
// title: "Charts",
// icon: Icons.PieChart,
// items: [
// {
// title: "Basic Chart",
// url: "/charts/basic-chart",
// },
// ],
// },
// {
// title: "UI Elements",
// icon: Icons.FourCircle,
// items: [
// {
// title: "Alerts",
// url: "/ui-elements/alerts",
// },
// {
// title: "Buttons",
// url: "/ui-elements/buttons",
// },
// ],
// },
// {
// title: "Authentication",
// icon: Icons.Authentication,
// items: [
// {
// title: "Sign In",
// url: "/auth/sign-in",
// },
// ],
// },
// ],
// },
];
@@ -0,0 +1,7 @@
export const UI_ELEMENTS = ["Alerts", "Buttons"]
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map((value) => ({
title: value,
url: "/ui-elements/" + value.split(" ").join("-").toLowerCase(),
isPro: !["Alerts", "Buttons"].includes(value),
}));
+202
View File
@@ -0,0 +1,202 @@
import { SVGProps } from "react";
export type PropsType = SVGProps<SVGSVGElement>;
export function ChevronUp(props: PropsType) {
return (
<svg
width={16}
height={8}
viewBox="0 0 16 8"
fill="currentColor"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.553.728a.687.687 0 01.895 0l6.416 5.5a.688.688 0 01-.895 1.044L8 2.155 2.03 7.272a.688.688 0 11-.894-1.044l6.417-5.5z"
/>
</svg>
);
}
export function Building(props: PropsType) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 -960 960 960"
fill="currentColor"
{...props}
>
<path d="M80-120v-720h400v160h400v560H80Zm80-80h240v-80H160v80Zm0-160h240v-80H160v80Zm0-160h240v-80H160v80Zm0-160h240v-80H160v80Zm320 480h320v-400H480v400Zm80-240v-80h160v80H560Zm0 160v-80h160v80H560Z" />
</svg>
);
}
export function HomeIcon(props: PropsType) {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path d="M9 17.25a.75.75 0 000 1.5h6a.75.75 0 000-1.5H9z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 1.25c-.725 0-1.387.2-2.11.537-.702.327-1.512.81-2.528 1.415l-1.456.867c-1.119.667-2.01 1.198-2.686 1.706C2.523 6.3 2 6.84 1.66 7.551c-.342.711-.434 1.456-.405 2.325.029.841.176 1.864.36 3.146l.293 2.032c.237 1.65.426 2.959.707 3.978.29 1.05.702 1.885 1.445 2.524.742.64 1.63.925 2.716 1.062 1.056.132 2.387.132 4.066.132h2.316c1.68 0 3.01 0 4.066-.132 1.086-.137 1.974-.422 2.716-1.061.743-.64 1.155-1.474 1.445-2.525.281-1.02.47-2.328.707-3.978l.292-2.032c.185-1.282.332-2.305.36-3.146.03-.87-.062-1.614-.403-2.325C22 6.84 21.477 6.3 20.78 5.775c-.675-.508-1.567-1.039-2.686-1.706l-1.456-.867c-1.016-.605-1.826-1.088-2.527-1.415-.724-.338-1.386-.537-2.111-.537zM8.096 4.511c1.057-.63 1.803-1.073 2.428-1.365.609-.284 1.047-.396 1.476-.396.43 0 .867.112 1.476.396.625.292 1.37.735 2.428 1.365l1.385.825c1.165.694 1.986 1.184 2.59 1.638.587.443.91.809 1.11 1.225.199.416.282.894.257 1.626-.026.75-.16 1.691-.352 3.026l-.28 1.937c-.246 1.714-.422 2.928-.675 3.845-.247.896-.545 1.415-.977 1.787-.433.373-.994.593-1.925.71-.951.119-2.188.12-3.93.12h-2.213c-1.743 0-2.98-.001-3.931-.12-.93-.117-1.492-.337-1.925-.71-.432-.372-.73-.891-.977-1.787-.253-.917-.43-2.131-.676-3.845l-.279-1.937c-.192-1.335-.326-2.277-.352-3.026-.025-.732.058-1.21.258-1.626.2-.416.521-.782 1.11-1.225.603-.454 1.424-.944 2.589-1.638l1.385-.825z"
/>
</svg>
);
}
export function Calendar(props: PropsType) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
d="M17 14a1 1 0 100-2 1 1 0 000 2zM17 18a1 1 0 100-2 1 1 0 000 2zM13 13a1 1 0 11-2 0 1 1 0 012 0zM13 17a1 1 0 11-2 0 1 1 0 012 0zM7 14a1 1 0 100-2 1 1 0 000 2zM7 18a1 1 0 100-2 1 1 0 000 2z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7 1.75a.75.75 0 01.75.75v.763c.662-.013 1.391-.013 2.193-.013h4.113c.803 0 1.532 0 2.194.013V2.5a.75.75 0 011.5 0v.827c.26.02.506.045.739.076 1.172.158 2.121.49 2.87 1.238.748.749 1.08 1.698 1.238 2.87.153 1.14.153 2.595.153 4.433v2.112c0 1.838 0 3.294-.153 4.433-.158 1.172-.49 2.121-1.238 2.87-.749.748-1.698 1.08-2.87 1.238-1.14.153-2.595.153-4.433.153H9.944c-1.838 0-3.294 0-4.433-.153-1.172-.158-2.121-.49-2.87-1.238-.748-.749-1.08-1.698-1.238-2.87-.153-1.14-.153-2.595-.153-4.433v-2.112c0-1.838 0-3.294.153-4.433.158-1.172.49-2.121 1.238-2.87.749-.748 1.698-1.08 2.87-1.238.233-.031.48-.056.739-.076V2.5A.75.75 0 017 1.75zM5.71 4.89c-1.005.135-1.585.389-2.008.812-.423.423-.677 1.003-.812 2.009-.023.17-.042.35-.058.539h18.336c-.016-.19-.035-.369-.058-.54-.135-1.005-.389-1.585-.812-2.008-.423-.423-1.003-.677-2.009-.812-1.027-.138-2.382-.14-4.289-.14h-4c-1.907 0-3.261.002-4.29.14zM2.75 12c0-.854 0-1.597.013-2.25h18.474c.013.653.013 1.396.013 2.25v2c0 1.907-.002 3.262-.14 4.29-.135 1.005-.389 1.585-.812 2.008-.423.423-1.003.677-2.009.812-1.027.138-2.382.14-4.289.14h-4c-1.907 0-3.261-.002-4.29-.14-1.005-.135-1.585-.389-2.008-.812-.423-.423-.677-1.003-.812-2.009-.138-1.027-.14-2.382-.14-4.289v-2z"
fill="currentColor"
/>
</svg>
);
}
export function User(props: PropsType) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 1.25a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zM8.75 6a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0zM12 12.25c-2.313 0-4.445.526-6.024 1.414C4.42 14.54 3.25 15.866 3.25 17.5v.102c-.001 1.162-.002 2.62 1.277 3.662.629.512 1.51.877 2.7 1.117 1.192.242 2.747.369 4.773.369s3.58-.127 4.774-.369c1.19-.24 2.07-.605 2.7-1.117 1.279-1.042 1.277-2.5 1.276-3.662V17.5c0-1.634-1.17-2.96-2.725-3.836-1.58-.888-3.711-1.414-6.025-1.414zM4.75 17.5c0-.851.622-1.775 1.961-2.528 1.316-.74 3.184-1.222 5.29-1.222 2.104 0 3.972.482 5.288 1.222 1.34.753 1.961 1.677 1.961 2.528 0 1.308-.04 2.044-.724 2.6-.37.302-.99.597-2.05.811-1.057.214-2.502.339-4.476.339-1.974 0-3.42-.125-4.476-.339-1.06-.214-1.68-.509-2.05-.81-.684-.557-.724-1.293-.724-2.601z"
fill="currentColor"
/>
</svg>
);
}
export function Alphabet(props: PropsType) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.25 7A.75.75 0 013 6.25h10a.75.75 0 010 1.5H3A.75.75 0 012.25 7zm14.25-.75a.75.75 0 01.684.442l4.5 10a.75.75 0 11-1.368.616l-1.437-3.194H14.12l-1.437 3.194a.75.75 0 11-1.368-.616l4.5-10a.75.75 0 01.684-.442zm-1.704 6.364h3.408L16.5 8.828l-1.704 3.786zM2.25 12a.75.75 0 01.75-.75h7a.75.75 0 010 1.5H3a.75.75 0 01-.75-.75zm0 5a.75.75 0 01.75-.75h5a.75.75 0 010 1.5H3a.75.75 0 01-.75-.75z"
fill="currentColor"
/>
</svg>
);
}
export function Table(props: PropsType) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18.29 4.89c-1.028-.138-2.383-.14-4.29-.14h-4c-1.907 0-3.261.002-4.29.14-1.005.135-1.585.389-2.008.812-.423.423-.677 1.003-.812 2.009-.138 1.028-.14 2.382-.14 4.289 0 1.907.002 3.261.14 4.29.135 1.005.389 1.585.812 2.008.423.423 1.003.677 2.009.812 1.028.138 2.382.14 4.289.14h4c1.907 0 3.262-.002 4.29-.14 1.005-.135 1.585-.389 2.008-.812.423-.423.677-1.003.812-2.009.138-1.028.14-2.382.14-4.289 0-1.907-.002-3.261-.14-4.29-.135-1.005-.389-1.585-.812-2.008-.423-.423-1.003-.677-2.009-.812zm.199-1.487c1.172.158 2.121.49 2.87 1.238.748.749 1.08 1.698 1.238 2.87.153 1.14.153 2.595.153 4.433v.112c0 1.838 0 3.294-.153 4.433-.158 1.172-.49 2.121-1.238 2.87-.749.748-1.698 1.08-2.87 1.238-1.14.153-2.595.153-4.433.153H9.944c-1.838 0-3.294 0-4.433-.153-1.172-.158-2.121-.49-2.87-1.238-.748-.749-1.08-1.698-1.238-2.87-.153-1.14-.153-2.595-.153-4.433v-.112c0-1.838 0-3.294.153-4.433.158-1.172.49-2.121 1.238-2.87.749-.748 1.698-1.08 2.87-1.238 1.14-.153 2.595-.153 4.433-.153h4.112c1.838 0 3.294 0 4.433.153zM8.25 17a.75.75 0 01.75-.75h6a.75.75 0 010 1.5H9a.75.75 0 01-.75-.75z"
/>
</svg>
);
}
export function PieChart(props: PropsType) {
return (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.254 1.365c-1.096-.306-2.122.024-2.851.695-.719.66-1.153 1.646-1.153 2.7v6.695a2.295 2.295 0 002.295 2.295h6.694c1.055 0 2.042-.434 2.701-1.153.67-.729 1.001-1.755.695-2.851a12.102 12.102 0 00-8.38-8.381zM11.75 4.76c0-.652.27-1.232.668-1.597.386-.355.886-.508 1.433-.355 3.55.991 6.349 3.79 7.34 7.34.153.548 0 1.048-.355 1.434-.365.397-.945.667-1.597.667h-6.694a.795.795 0 01-.795-.795V4.761z"
fill="currentColor"
/>
<path
d="M8.672 4.716a.75.75 0 00-.45-1.432C4.183 4.554 1.25 8.328 1.25 12.79c0 5.501 4.46 9.961 9.96 9.961 4.462 0 8.236-2.932 9.505-6.973a.75.75 0 10-1.43-.45 8.465 8.465 0 01-8.074 5.923 8.46 8.46 0 01-8.461-8.46 8.465 8.465 0 015.922-8.074z"
fill="currentColor"
/>
</svg>
);
}
export function FourCircle(props: PropsType) {
return (
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.5 1.75a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zM3.25 6.5a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0zM17.5 12.75a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zm-3.25 4.75a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0zM12.75 6.5a4.75 4.75 0 119.5 0 4.75 4.75 0 01-9.5 0zm4.75-3.25a3.25 3.25 0 100 6.5 3.25 3.25 0 000-6.5zM6.5 12.75a4.75 4.75 0 100 9.5 4.75 4.75 0 000-9.5zM3.25 17.5a3.25 3.25 0 116.5 0 3.25 3.25 0 01-6.5 0z"
fill="currentColor"
/>
</svg>
);
}
export function Authentication(props: PropsType) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<path d="M14.945 1.25c-1.367 0-2.47 0-3.337.117-.9.12-1.658.38-2.26.981-.524.525-.79 1.17-.929 1.928-.135.737-.161 1.638-.167 2.72a.75.75 0 001.5.008c.006-1.093.034-1.868.142-2.457.105-.566.272-.895.515-1.138.277-.277.666-.457 1.4-.556.755-.101 1.756-.103 3.191-.103h1c1.436 0 2.437.002 3.192.103.734.099 1.122.28 1.4.556.276.277.456.665.555 1.4.102.754.103 1.756.103 3.191v8c0 1.435-.001 2.436-.103 3.192-.099.734-.279 1.122-.556 1.399-.277.277-.665.457-1.399.556-.755.101-1.756.103-3.192.103h-1c-1.435 0-2.436-.002-3.192-.103-.733-.099-1.122-.28-1.399-.556-.243-.244-.41-.572-.515-1.138-.108-.589-.136-1.364-.142-2.457a.75.75 0 10-1.5.008c.006 1.082.032 1.983.167 2.72.14.758.405 1.403.93 1.928.601.602 1.36.86 2.26.982.866.116 1.969.116 3.336.116h1.11c1.368 0 2.47 0 3.337-.116.9-.122 1.658-.38 2.26-.982.602-.602.86-1.36.982-2.26.116-.867.116-1.97.116-3.337v-8.11c0-1.367 0-2.47-.116-3.337-.121-.9-.38-1.658-.982-2.26-.602-.602-1.36-.86-2.26-.981-.867-.117-1.97-.117-3.337-.117h-1.11z" />
<path d="M2.001 11.249a.75.75 0 000 1.5h11.973l-1.961 1.68a.75.75 0 10.976 1.14l3.5-3a.75.75 0 000-1.14l-3.5-3a.75.75 0 00-.976 1.14l1.96 1.68H2.002z" />
</svg>
);
}
export function ArrowLeftIcon(props: PropsType) {
return (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="currentColor"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.89775 4.10225C8.11742 4.32192 8.11742 4.67808 7.89775 4.89775L4.358 8.4375H15C15.3107 8.4375 15.5625 8.68934 15.5625 9C15.5625 9.31066 15.3107 9.5625 15 9.5625H4.358L7.89775 13.1023C8.11742 13.3219 8.11742 13.6781 7.89775 13.8977C7.67808 14.1174 7.32192 14.1174 7.10225 13.8977L2.60225 9.39775C2.38258 9.17808 2.38258 8.82192 2.60225 8.60225L7.10225 4.10225C7.32192 3.88258 7.67808 3.88258 7.89775 4.10225Z"
fill=""
/>
</svg>
);
}
+181
View File
@@ -0,0 +1,181 @@
"use client";
import { Logo } from "@/components/logo";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { NAV_DATA, NavSubItem } from "./data";
import { ArrowLeftIcon, ChevronUp } from "./icons";
import { MenuItem } from "./menu-item";
import { useSidebarContext } from "./sidebar-context";
import Image from "next/image";
export function Sidebar() {
const pathname = usePathname();
const { setIsOpen, isOpen, isMobile, toggleSidebar } = useSidebarContext();
const [expandedItems, setExpandedItems] = useState<string[]>([]);
const toggleExpanded = (title: string) => {
// setExpandedItems((prev) => (prev.includes(title) ? [] : [title]));
// Uncomment the following line to enable multiple expanded items
setExpandedItems((prev) =>
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title],
);
};
useEffect(() => {
// Keep collapsible open, when it's subpage is active
NAV_DATA.some((section) => {
return section.items.some((item) => {
return item.items.some((subItem: NavSubItem) => {
if (subItem.url === pathname) {
if (!expandedItems.includes(item.title)) {
toggleExpanded(item.title);
}
// Break the loop
return true;
}
});
});
});
}, [pathname]);
return (
<>
{/* Mobile Overlay */}
{isMobile && isOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 transition-opacity duration-300"
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
)}
<aside
className={cn(
"max-w-[290px] overflow-hidden border-r border-gray-200 bg-white transition-[width] duration-200 ease-linear dark:border-gray-800 dark:bg-gray-dark",
isMobile ? "fixed bottom-0 top-0 z-50" : "sticky top-0 h-screen",
isOpen ? "w-full" : "w-0",
)}
aria-label="Main navigation"
aria-hidden={!isOpen}
inert={!isOpen}
>
<div className="flex h-full flex-col py-10 pl-[25px] pr-[7px]">
<div className="relative pr-4.5">
<Link
href={"/"}
onClick={() => isMobile && toggleSidebar()}
className="px-0 py-2.5 min-[850px]:py-0"
>
<Logo />
</Link>
{isMobile && (
<button
onClick={toggleSidebar}
className="absolute left-3/4 right-4.5 top-1/2 -translate-y-1/2 text-right"
>
<span className="sr-only">Close Menu</span>
<ArrowLeftIcon className="ml-auto size-7" />
</button>
)}
</div>
{/* Navigation */}
<div className="custom-scrollbar mt-6 flex-1 overflow-y-auto pr-3 min-[850px]:mt-10">
{NAV_DATA.map((section) => (
<div key={section.label} className="mb-6">
<h2 className="mb-5 text-sm font-medium text-dark-4 dark:text-dark-6">
{section.label}
</h2>
<nav role="navigation" aria-label={section.label}>
<ul className="space-y-2">
{section.items.map((item) => (
<li key={item.title}>
{item.items.length ? (
<div>
<MenuItem
isActive={item.items.some(
({ url }: { url: string }) => url === pathname,
)}
onClick={() => toggleExpanded(item.title)}
>
<item.icon
className="size-6 shrink-0"
aria-hidden="true"
/>
<span>{item.title}</span>
<ChevronUp
className={cn(
"ml-auto rotate-180 transition-transform duration-200",
expandedItems.includes(item.title) &&
"rotate-0",
)}
aria-hidden="true"
/>
</MenuItem>
{expandedItems.includes(item.title) && (
<ul
className="ml-9 mr-0 space-y-1.5 pb-[15px] pr-0 pt-2"
role="menu"
>
{item.items.map((subItem: NavSubItem) => (
<li key={subItem.title} role="none">
<MenuItem
as="link"
href={subItem.url}
isActive={pathname === subItem.url}
>
<span>{subItem.title}</span>
</MenuItem>
</li>
))}
</ul>
)}
</div>
) : (
(() => {
const href =
"url" in item
? item.url + ""
: "/" +
item.title.toLowerCase().split(" ").join("-");
return (
<MenuItem
className="flex items-center gap-3 py-3"
as="link"
href={href}
isActive={pathname === href}
>
<item.icon
className="size-6 shrink-0"
aria-hidden="true"
/>
<span>{item.title}</span>
</MenuItem>
);
})()
)}
</li>
))}
</ul>
</nav>
</div>
))}
</div>
</div>
</aside>
</>
);
}
@@ -0,0 +1,62 @@
import { cn } from "@/lib/utils";
import { cva } from "class-variance-authority";
import Link from "next/link";
import { useSidebarContext } from "./sidebar-context";
const menuItemBaseStyles = cva(
"rounded-lg px-3.5 font-medium text-dark-4 transition-all duration-200 dark:text-dark-6",
{
variants: {
isActive: {
true: "bg-[rgba(87,80,241,0.07)] text-primary hover:bg-[rgba(87,80,241,0.07)] dark:bg-[#FFFFFF1A] dark:text-white",
false:
"hover:bg-gray-100 hover:text-dark hover:dark:bg-[#FFFFFF1A] hover:dark:text-white",
},
},
defaultVariants: {
isActive: false,
},
},
);
export function MenuItem(
props: {
className?: string;
children: React.ReactNode;
isActive: boolean;
} & ({ as?: "button"; onClick: () => void } | { as: "link"; href: string }),
) {
const { toggleSidebar, isMobile } = useSidebarContext();
if (props.as === "link") {
return (
<Link
href={props.href}
// Close sidebar on clicking link if it's mobile
onClick={() => isMobile && toggleSidebar()}
className={cn(
menuItemBaseStyles({
isActive: props.isActive,
className: "relative block py-2",
}),
props.className,
)}
>
{props.children}
</Link>
);
}
return (
<button
onClick={props.onClick}
aria-expanded={props.isActive}
className={menuItemBaseStyles({
isActive: props.isActive,
className: "flex w-full items-center gap-3 py-3",
})}
>
{props.children}
</button>
);
}
@@ -0,0 +1,61 @@
"use client";
import { useIsMobile } from "@/hooks/use-mobile";
import { createContext, useContext, useEffect, useState } from "react";
type SidebarState = "expanded" | "collapsed";
type SidebarContextType = {
state: SidebarState;
isOpen: boolean;
setIsOpen: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = createContext<SidebarContextType | null>(null);
export function useSidebarContext() {
const context = useContext(SidebarContext);
if (!context) {
throw new Error("useSidebarContext must be used within a SidebarProvider");
}
return context;
}
export function SidebarProvider({
children,
defaultOpen = true,
}: {
children: React.ReactNode;
defaultOpen?: boolean;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const isMobile = useIsMobile();
useEffect(() => {
if (isMobile) {
setIsOpen(false);
} else {
setIsOpen(true);
}
}, [isMobile]);
function toggleSidebar() {
setIsOpen((prev) => !prev);
}
return (
<SidebarContext.Provider
value={{
state: isOpen ? "expanded" : "collapsed",
isOpen,
setIsOpen,
isMobile,
toggleSidebar,
}}
>
{children}
</SidebarContext.Provider>
);
}