Initial commit for new panel
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Header } from "./header";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { AuthGuard, LoginGuard } from "@/contexts";
|
||||
|
||||
export function ConditionalLayout({ children }: PropsWithChildren) {
|
||||
const pathname = usePathname();
|
||||
const isAuthPage = pathname.startsWith("/auth");
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAuthPage ? (
|
||||
<LoginGuard>
|
||||
<main>{children}</main>
|
||||
</LoginGuard>
|
||||
) : (
|
||||
<AuthGuard>
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<div className="w-full bg-gray-2 dark:bg-[#020d1a]">
|
||||
<Header />
|
||||
<main className="isolate mx-auto w-full max-w-screen-2xl overflow-hidden p-4 md:p-6 2xl:p-10">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AuthGuard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { IconProps } from "@/types/icon-props";
|
||||
|
||||
export function MenuIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M3.5625 6C3.5625 5.58579 3.89829 5.25 4.3125 5.25H20.3125C20.7267 5.25 21.0625 5.58579 21.0625 6C21.0625 6.41421 20.7267 6.75 20.3125 6.75L4.3125 6.75C3.89829 6.75 3.5625 6.41422 3.5625 6Z" />
|
||||
<path d="M3.5625 18C3.5625 17.5858 3.89829 17.25 4.3125 17.25L20.3125 17.25C20.7267 17.25 21.0625 17.5858 21.0625 18C21.0625 18.4142 20.7267 18.75 20.3125 18.75L4.3125 18.75C3.89829 18.75 3.5625 18.4142 3.5625 18Z" />
|
||||
<path d="M4.3125 11.25C3.89829 11.25 3.5625 11.5858 3.5625 12C3.5625 12.4142 3.89829 12.75 4.3125 12.75L20.3125 12.75C20.7267 12.75 21.0625 12.4142 21.0625 12C21.0625 11.5858 20.7267 11.25 20.3125 11.25L4.3125 11.25Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useSidebarContext } from "../sidebar/sidebar-context";
|
||||
import { MenuIcon } from "./icons";
|
||||
import { Notification } from "./notification";
|
||||
import { ThemeToggleSwitch } from "./theme-toggle";
|
||||
import { UserInfo } from "./user-info";
|
||||
|
||||
export function Header() {
|
||||
const { toggleSidebar, isMobile } = useSidebarContext();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 flex items-center justify-between border-b border-stroke bg-white px-4 py-5 shadow-1 dark:border-stroke-dark dark:bg-gray-dark md:px-5 2xl:px-10">
|
||||
<button
|
||||
onClick={toggleSidebar}
|
||||
className="rounded-lg border px-1.5 py-1 dark:border-stroke-dark dark:bg-[#020D1A] hover:dark:bg-[#FFFFFF1A] lg:hidden"
|
||||
>
|
||||
<MenuIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</button>
|
||||
|
||||
{isMobile && (
|
||||
<Link href={"/"} className="ml-2 max-[430px]:hidden min-[375px]:ml-4">
|
||||
<Image
|
||||
src={"/images/logo/logo-icon.svg"}
|
||||
width={32}
|
||||
height={32}
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div className="max-xl:hidden">
|
||||
<h1 className="mb-0.5 text-heading-5 font-bold text-dark dark:text-white">
|
||||
Opportunity Lens
|
||||
</h1>
|
||||
<p className="font-medium">Opportunity Lens management panel</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-end gap-2 min-[375px]:gap-4">
|
||||
<ThemeToggleSwitch />
|
||||
|
||||
<Notification />
|
||||
|
||||
<div className="shrink-0">
|
||||
<UserInfo />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function BellIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width={20} height={20} viewBox="0 0 20 20" fill="none" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 1.042A6.458 6.458 0 003.542 7.5v.587c0 .58-.172 1.148-.495 1.631l-.957 1.436a2.934 2.934 0 001.67 4.459c.63.171 1.264.316 1.903.435l.002.005c.64 1.71 2.353 2.905 4.335 2.905 1.982 0 3.694-1.196 4.335-2.905l.002-.005a23.736 23.736 0 001.903-.435 2.934 2.934 0 001.67-4.459l-.958-1.436a2.941 2.941 0 01-.494-1.631V7.5A6.458 6.458 0 0010 1.042zm2.813 15.239a23.71 23.71 0 01-5.627 0c.593.85 1.623 1.427 2.814 1.427 1.19 0 2.221-.576 2.813-1.427zM4.792 7.5a5.208 5.208 0 1110.416 0v.587c0 .827.245 1.636.704 2.325l.957 1.435c.638.957.151 2.257-.958 2.56a22.467 22.467 0 01-11.822 0 1.684 1.684 0 01-.959-2.56l.958-1.435a4.192 4.192 0 00.704-2.325V7.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownContent,
|
||||
DropdownTrigger,
|
||||
} from "@/components/ui/dropdown";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { BellIcon } from "./icons";
|
||||
|
||||
const notificationList = [
|
||||
{
|
||||
image: "/images/user/user-15.png",
|
||||
title: "Piter Joined the Team!",
|
||||
subTitle: "Congratulate him",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-03.png",
|
||||
title: "New message",
|
||||
subTitle: "Devid sent a new message",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-26.png",
|
||||
title: "New Payment received",
|
||||
subTitle: "Check your earnings",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-28.png",
|
||||
title: "Jolly completed tasks",
|
||||
subTitle: "Assign new task",
|
||||
},
|
||||
{
|
||||
image: "/images/user/user-27.png",
|
||||
title: "Roman Joined the Team!",
|
||||
subTitle: "Congratulate him",
|
||||
},
|
||||
];
|
||||
|
||||
export function Notification() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDotVisible, setIsDotVisible] = useState(true);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
setIsOpen={(open) => {
|
||||
setIsOpen(open);
|
||||
|
||||
if (setIsDotVisible) setIsDotVisible(false);
|
||||
}}
|
||||
>
|
||||
<DropdownTrigger
|
||||
className="grid size-12 place-items-center rounded-full border bg-gray-2 text-dark outline-none hover:text-primary focus-visible:border-primary focus-visible:text-primary dark:border-dark-4 dark:bg-dark-3 dark:text-white dark:focus-visible:border-primary"
|
||||
aria-label="View Notifications"
|
||||
>
|
||||
<span className="relative">
|
||||
<BellIcon />
|
||||
|
||||
{isDotVisible && (
|
||||
<span
|
||||
className={cn(
|
||||
"absolute right-0 top-0 z-1 size-2 rounded-full bg-red-light ring-2 ring-gray-2 dark:ring-dark-3",
|
||||
)}
|
||||
>
|
||||
<span className="absolute inset-0 -z-1 animate-ping rounded-full bg-red-light opacity-75" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent
|
||||
align={isMobile ? "end" : "center"}
|
||||
className="border border-stroke bg-white px-3.5 py-3 shadow-md dark:border-dark-3 dark:bg-gray-dark min-[350px]:min-w-[20rem]"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between px-2 py-1.5">
|
||||
<span className="text-lg font-medium text-dark dark:text-white">
|
||||
Notifications
|
||||
</span>
|
||||
<span className="rounded-md bg-primary px-[9px] py-0.5 text-xs font-medium text-white">
|
||||
5 new
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul className="mb-3 max-h-[23rem] space-y-1.5 overflow-y-auto">
|
||||
{notificationList.map((item, index) => (
|
||||
<li key={index} role="menuitem">
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex items-center gap-4 rounded-lg px-2 py-1.5 outline-none hover:bg-gray-2 focus-visible:bg-gray-2 dark:hover:bg-dark-3 dark:focus-visible:bg-dark-3"
|
||||
>
|
||||
<Image
|
||||
src={item.image}
|
||||
className="size-14 rounded-full object-cover"
|
||||
width={200}
|
||||
height={200}
|
||||
alt="User"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<strong className="block text-sm font-medium text-dark dark:text-white">
|
||||
{item.title}
|
||||
</strong>
|
||||
|
||||
<span className="truncate text-sm font-medium text-dark-5 dark:text-dark-6">
|
||||
{item.subTitle}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="block rounded-lg border border-primary p-2 text-center text-sm font-medium tracking-wide text-primary outline-none transition-colors hover:bg-blue-light-5 focus:bg-blue-light-5 focus:text-primary focus-visible:border-primary dark:border-dark-3 dark:text-dark-6 dark:hover:border-dark-5 dark:hover:bg-dark-3 dark:hover:text-dark-7 dark:focus-visible:border-dark-5 dark:focus-visible:bg-dark-3 dark:focus-visible:text-dark-7"
|
||||
>
|
||||
See all notifications
|
||||
</Link>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function Sun(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M10 1.042c.345 0 .625.28.625.625V2.5a.625.625 0 11-1.25 0v-.833c0-.346.28-.625.625-.625zM3.666 3.665a.625.625 0 01.883 0l.328.328a.625.625 0 01-.884.884l-.327-.328a.625.625 0 010-.884zm12.668 0a.625.625 0 010 .884l-.327.328a.625.625 0 01-.884-.884l.327-.327a.625.625 0 01.884 0zM10 5.626a4.375 4.375 0 100 8.75 4.375 4.375 0 000-8.75zM4.375 10a5.625 5.625 0 1111.25 0 5.625 5.625 0 01-11.25 0zm-3.333 0c0-.345.28-.625.625-.625H2.5a.625.625 0 110 1.25h-.833A.625.625 0 011.042 10zm15.833 0c0-.345.28-.625.625-.625h.833a.625.625 0 010 1.25H17.5a.625.625 0 01-.625-.625zm-1.752 5.123a.625.625 0 01.884 0l.327.327a.625.625 0 11-.884.884l-.327-.327a.625.625 0 010-.884zm-10.246 0a.625.625 0 010 .884l-.328.327a.625.625 0 11-.883-.884l.327-.327a.625.625 0 01.884 0zM10 16.875c.345 0 .625.28.625.625v.833a.625.625 0 01-1.25 0V17.5c0-.345.28-.625.625-.625z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function Moon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.18 2.334a7.71 7.71 0 108.485 8.485A6.042 6.042 0 119.18 2.335zM1.042 10a8.958 8.958 0 018.958-8.958c.598 0 .896.476.948.855.049.364-.086.828-.505 1.082a4.792 4.792 0 106.579 6.579c.253-.42.717-.555 1.081-.506.38.052.856.35.856.948A8.958 8.958 0 011.04 10z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Moon, Sun } from "./icons";
|
||||
|
||||
const THEMES = [
|
||||
{
|
||||
name: "light",
|
||||
Icon: Sun,
|
||||
},
|
||||
{
|
||||
name: "dark",
|
||||
Icon: Moon,
|
||||
},
|
||||
];
|
||||
|
||||
export function ThemeToggleSwitch() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
className="group rounded-full bg-gray-3 p-[5px] text-[#111928] outline-1 outline-primary focus-visible:outline dark:bg-[#020D1A] dark:text-current"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Switch to {theme === "light" ? "dark" : "light"} mode
|
||||
</span>
|
||||
|
||||
<span aria-hidden className="relative flex gap-2.5">
|
||||
{/* Indicator */}
|
||||
<span className="absolute size-[38px] rounded-full border border-gray-200 bg-white transition-all dark:translate-x-[48px] dark:border-none dark:bg-dark-2 dark:group-hover:bg-dark-3" />
|
||||
|
||||
{THEMES.map(({ name, Icon }) => (
|
||||
<span
|
||||
key={name}
|
||||
className={cn(
|
||||
"relative grid size-[38px] place-items-center rounded-full",
|
||||
name === "dark" && "dark:text-white",
|
||||
)}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
type SVGPropsType = SVGProps<SVGSVGElement>;
|
||||
|
||||
export function UserIcon(props: SVGPropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 .938a3.562 3.562 0 100 7.124A3.562 3.562 0 009 .938zM6.562 4.5a2.437 2.437 0 114.875 0 2.437 2.437 0 01-4.875 0zM9 9.188c-1.735 0-3.334.394-4.518 1.06-1.167.657-2.045 1.652-2.045 2.877v.076c0 .872-.001 1.965.958 2.747.472.384 1.132.657 2.025.838.894.181 2.06.276 3.58.276s2.685-.095 3.58-.276c.893-.18 1.553-.454 2.025-.838.96-.782.958-1.875.957-2.747v-.076c0-1.226-.877-2.22-2.044-2.877-1.184-.666-2.783-1.06-4.518-1.06zm-5.438 3.937c0-.639.467-1.331 1.471-1.896.987-.555 2.388-.916 3.967-.916 1.579 0 2.98.36 3.967.916 1.004.565 1.47 1.258 1.47 1.896 0 .98-.03 1.533-.542 1.95-.278.227-.743.448-1.538.609-.793.16-1.876.254-3.357.254-1.48 0-2.564-.094-3.357-.255-.795-.16-1.26-.381-1.538-.608-.512-.417-.543-.97-.543-1.95z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsIcon(props: SVGPropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 6.188a2.813 2.813 0 100 5.625 2.813 2.813 0 000-5.626zM7.312 9a1.688 1.688 0 113.376 0 1.688 1.688 0 01-3.376 0z"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.981.938c-.333 0-.612 0-.84.015a2.117 2.117 0 00-.68.142c-.506.209-.907.61-1.117 1.116-.108.263-.138.54-.15.841a.65.65 0 01-.311.55.65.65 0 01-.631-.005c-.267-.141-.522-.254-.804-.291a2.063 2.063 0 00-1.525.408c-.187.144-.33.32-.462.519-.128.19-.267.43-.434.72l-.019.032c-.166.289-.306.53-.406.735a2.117 2.117 0 00-.218.66c-.071.543.076 1.091.409 1.525.173.226.398.39.654.55A.65.65 0 012.766 9a.65.65 0 01-.32.544c-.255.16-.48.325-.653.55-.333.435-.48.983-.409 1.526.03.233.113.445.218.66.1.205.24.446.406.735l.02.033c.166.288.305.53.433.72.133.197.275.374.462.518.434.333.983.48 1.525.408.282-.037.537-.15.804-.29a.65.65 0 01.63-.005.65.65 0 01.313.549c.011.3.04.578.15.841.209.506.61.907 1.116 1.117.217.09.442.125.68.14.228.017.507.017.84.017h.038c.333 0 .612 0 .84-.016.238-.016.463-.051.68-.142.506-.209.907-.61 1.117-1.116.108-.263.138-.54.15-.841a.65.65 0 01.311-.55.65.65 0 01.631.005c.267.141.522.254.804.291a2.062 2.062 0 001.525-.408c.187-.144.33-.32.462-.519.128-.19.267-.43.434-.72l.019-.032c.166-.289.305-.53.406-.736.105-.214.187-.426.218-.66a2.062 2.062 0 00-.409-1.524c-.173-.226-.398-.39-.654-.55A.65.65 0 0115.234 9a.65.65 0 01.32-.544c.255-.16.48-.325.653-.55.333-.435.48-.983.409-1.526a2.117 2.117 0 00-.218-.66c-.1-.205-.24-.446-.406-.735l-.02-.033c-.166-.288-.305-.53-.433-.72a2.117 2.117 0 00-.462-.518 2.062 2.062 0 00-1.525-.408c-.282.037-.537.15-.804.29a.65.65 0 01-.63.005.65.65 0 01-.313-.549c-.011-.3-.04-.578-.15-.841a2.063 2.063 0 00-1.116-1.116 2.118 2.118 0 00-.68-.142c-.228-.016-.507-.016-.84-.015H8.98zm-1.09 1.196c.058-.024.146-.046.327-.059.185-.012.425-.013.782-.013.357 0 .597 0 .782.013.181.013.269.035.327.059.23.095.412.278.507.507.03.073.055.186.065.453.022.595.329 1.167.874 1.481a1.775 1.775 0 001.719.016c.237-.125.347-.16.425-.17a.938.938 0 01.693.186c.05.038.113.103.214.253.103.155.223.362.402.671.179.31.298.517.38.684.08.163.104.25.113.312a.937.937 0 01-.186.693c-.048.062-.133.14-.36.283A1.775 1.775 0 0014.109 9c0 .629.342 1.18.846 1.497.227.143.312.22.36.283a.938.938 0 01.186.693c-.009.062-.033.15-.113.312-.082.167-.201.374-.38.684-.179.309-.299.516-.402.67-.101.151-.165.216-.214.254a.937.937 0 01-.693.186c-.078-.01-.188-.045-.425-.17a1.775 1.775 0 00-1.72.016 1.775 1.775 0 00-.873 1.48c-.01.268-.035.381-.065.454a.937.937 0 01-.507.507 1.034 1.034 0 01-.327.059c-.185.012-.425.012-.782.012-.357 0-.597 0-.782-.012a1.033 1.033 0 01-.327-.059.937.937 0 01-.507-.507c-.03-.073-.055-.186-.065-.454a1.775 1.775 0 00-.874-1.48 1.775 1.775 0 00-1.719-.016c-.237.125-.347.16-.425.17a.937.937 0 01-.693-.186 1.034 1.034 0 01-.214-.253 12.818 12.818 0 01-.402-.671c-.179-.31-.298-.517-.38-.684a1.035 1.035 0 01-.113-.312.937.937 0 01.186-.693c.048-.063.133-.14.36-.283.504-.316.846-.868.846-1.497 0-.629-.342-1.18-.846-1.497-.227-.143-.312-.22-.36-.283a.937.937 0 01-.186-.693c.009-.062.033-.15.113-.312.082-.167.201-.375.38-.684.179-.31.299-.517.402-.67.101-.151.165-.216.214-.254a.938.938 0 01.693-.186c.078.01.188.045.425.17a1.775 1.775 0 001.72-.016c.544-.314.85-.886.873-1.48.01-.268.035-.381.065-.454a.937.937 0 01.507-.507z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogOutIcon(props: SVGPropsType) {
|
||||
return (
|
||||
<svg
|
||||
width={20}
|
||||
height={20}
|
||||
viewBox="0 0 18 18"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<g clipPath="url(#clip0_7095_11691)">
|
||||
<path d="M11.209.938c-1.026 0-1.852 0-2.503.087-.675.09-1.243.285-1.695.736-.393.394-.592.878-.697 1.446-.101.553-.12 1.229-.125 2.04a.562.562 0 101.125.006c.005-.82.026-1.401.107-1.842.078-.426.203-.672.386-.854.207-.208.499-.343 1.05-.417.566-.076 1.317-.078 2.393-.078H12c1.077 0 1.828.002 2.394.078.55.074.842.21 1.05.417.207.207.342.499.416 1.05.077.566.078 1.316.078 2.393v6c0 1.077-.002 1.827-.078 2.394-.074.55-.209.842-.417 1.05-.207.207-.499.342-1.049.416-.566.076-1.317.078-2.394.078h-.75c-1.076 0-1.827-.002-2.394-.078-.55-.074-.842-.21-1.05-.417-.182-.182-.307-.428-.385-.854-.081-.44-.102-1.022-.107-1.842a.563.563 0 00-1.125.006c.004.811.024 1.487.125 2.04.105.568.304 1.052.697 1.446.452.451 1.02.645 1.695.736.65.087 1.477.087 2.503.087h.832c1.026 0 1.853 0 2.503-.087.675-.09 1.243-.285 1.695-.736.451-.452.645-1.02.736-1.695.088-.65.088-1.477.088-2.503V5.96c0-1.026 0-1.853-.088-2.503-.09-.675-.285-1.243-.736-1.695-.452-.451-1.02-.645-1.695-.736-.65-.088-1.477-.088-2.503-.087h-.832z" />
|
||||
<path d="M11.25 8.438a.562.562 0 110 1.124H3.02l1.471 1.26a.563.563 0 01-.732.855l-2.625-2.25a.562.562 0 010-.854l2.625-2.25a.562.562 0 11.732.854l-1.47 1.26h8.229z" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7095_11691">
|
||||
<rect width={18} height={18} rx={5} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronUpIcon } from "@/assets/icons";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownContent,
|
||||
DropdownTrigger,
|
||||
} from "@/components/ui/dropdown";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { LogOutIcon, SettingsIcon, UserIcon } from "./icons";
|
||||
|
||||
export function UserInfo() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const USER = {
|
||||
name: "John Smith",
|
||||
email: "johnson@nextadmin.com",
|
||||
img: "/images/user/user-03.png",
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={isOpen} setIsOpen={setIsOpen}>
|
||||
<DropdownTrigger className="rounded align-middle outline-none ring-primary ring-offset-2 focus-visible:ring-1 dark:ring-offset-gray-dark">
|
||||
<span className="sr-only">My Account</span>
|
||||
|
||||
<figure className="flex items-center gap-3">
|
||||
<Image
|
||||
src={USER.img}
|
||||
className="size-12"
|
||||
alt={`Avatar of ${USER.name}`}
|
||||
role="presentation"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
<figcaption className="flex items-center gap-1 font-medium text-dark dark:text-dark-6 max-[1024px]:sr-only">
|
||||
<span>{USER.name}</span>
|
||||
|
||||
<ChevronUpIcon
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"rotate-180 transition-transform",
|
||||
isOpen && "rotate-0",
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</figcaption>
|
||||
</figure>
|
||||
</DropdownTrigger>
|
||||
|
||||
<DropdownContent
|
||||
className="border border-stroke bg-white shadow-md dark:border-dark-3 dark:bg-gray-dark min-[230px]:min-w-[17.5rem]"
|
||||
align="end"
|
||||
>
|
||||
<h2 className="sr-only">User information</h2>
|
||||
|
||||
<figure className="flex items-center gap-2.5 px-5 py-3.5">
|
||||
<Image
|
||||
src={USER.img}
|
||||
className="size-12"
|
||||
alt={`Avatar for ${USER.name}`}
|
||||
role="presentation"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
|
||||
<figcaption className="space-y-1 text-base font-medium">
|
||||
<div className="mb-2 leading-none text-dark dark:text-white">
|
||||
{USER.name}
|
||||
</div>
|
||||
|
||||
<div className="leading-none text-gray-6">{USER.email}</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
<hr className="border-[#E8E8E8] dark:border-dark-3" />
|
||||
|
||||
<div className="p-2 text-base text-[#4B5563] dark:text-dark-6 [&>*]:cursor-pointer">
|
||||
<Link
|
||||
href={"/profile"}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-[9px] hover:bg-gray-2 hover:text-dark dark:hover:bg-dark-3 dark:hover:text-white"
|
||||
>
|
||||
<UserIcon />
|
||||
|
||||
<span className="mr-auto text-base font-medium">View profile</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={"/pages/settings"}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-[9px] hover:bg-gray-2 hover:text-dark dark:hover:bg-dark-3 dark:hover:text-white"
|
||||
>
|
||||
<SettingsIcon />
|
||||
|
||||
<span className="mr-auto text-base font-medium">
|
||||
Account Settings
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<hr className="border-[#E8E8E8] dark:border-dark-3" />
|
||||
|
||||
<div className="p-2 text-base text-[#4B5563] dark:text-dark-6">
|
||||
<button
|
||||
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-[9px] hover:bg-gray-2 hover:text-dark dark:hover:bg-dark-3 dark:hover:text-white"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<LogOutIcon />
|
||||
|
||||
<span className="text-base font-medium">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</DropdownContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type PropsType = {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ShowcaseSection({ title, children, className }: PropsType) {
|
||||
return (
|
||||
<div className="rounded-[10px] bg-white shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||
<h2 className="border-b border-stroke px-4 py-4 font-medium text-dark dark:border-dark-3 dark:text-white sm:px-6 xl:px-7.5">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div className={cn("p-4 sm:p-6 xl:p-10", className)}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
}));
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user