feat: set basic sidebar and the main layout

This commit is contained in:
Pablo Lara
2024-07-11 10:31:06 +02:00
parent 160990f979
commit 746b427943
12 changed files with 1327 additions and 68 deletions
+7 -6
View File
@@ -1,12 +1,13 @@
import "@/styles/globals.css";
import { Navbar } from "@nextui-org/react";
import clsx from "clsx";
import { Metadata, Viewport } from "next";
import React from "react";
import { fontSans } from "@/config/fonts";
import { siteConfig } from "@/config/site";
import { SidebarWrap } from "../components/sidebar/SidebarWrap";
import { Providers } from "./providers";
export const metadata: Metadata = {
@@ -42,11 +43,11 @@ export default function RootLayout({
)}
>
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
<div className="relative flex flex-col h-screen">
<Navbar />
<main className="container mx-auto max-w-7xl pt-16 px-6 flex-grow">
{children}
</main>
<div className="flex items-center dark h-screen justify-center overflow-hidden">
<div className="flex h-dvh w-full">
<SidebarWrap />
<main className="w-full flex-1 flex-col p-4">{children}</main>
</div>
</div>
</Providers>
</body>
+168 -56
View File
@@ -1,65 +1,177 @@
"use client";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
import {
Button,
Card,
CardBody,
CardFooter,
CardHeader,
Input,
Link,
} from "@nextui-org/react";
import { Icon } from "@iconify/react";
import { Button } from "@nextui-org/react";
import React from "react";
export default function Home() {
const [isVisible, setIsVisible] = React.useState(false);
const toggleVisibility = () => setIsVisible(!isVisible);
return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10">
<div className="inline-block max-w-lg text-center justify-center">
<Card>
<CardHeader className="pb-0">
<h1 className="pl-3">Login</h1>
</CardHeader>
<CardBody className="w-72 pb-0">
<Input
label="Email"
variant="bordered"
placeholder="Enter your email"
className="mb-2"
<>
<main className="h-full w-full overflow-visible ">
<header className="flex items-center gap-3 rounded-medium border-small border-divider p-4">
<Button isIconOnly size="sm" variant="light">
<Icon
className="text-default-500"
height={24}
icon="solar:sidebar-minimalistic-outline"
width={24}
/>
<Input
label="Password"
variant="bordered"
placeholder="Enter your password"
endContent={
<button
className="focus:outline-none"
type="button"
onClick={toggleVisibility}
>
{isVisible ? (
<EyeSlashIcon className="text-2xl text-default-400 pointer-events-none w-5 h-5" />
) : (
<EyeIcon className="text-2xl text-default-400 pointer-events-none w-5 h-5" />
)}
</button>
}
type={isVisible ? "text" : "password"}
className="max-w-xs"
/>
</CardBody>
<CardFooter>
<Button href="/clouds" as={Link} color="primary">
Submit
</Button>
</CardFooter>
</Card>
<p className="mt-24">This is a page with "use client", useState</p>
</div>
</section>
</Button>
<h2 className="text-medium font-medium text-default-700">Overview</h2>
</header>
<div className="flex h-dvh-minus-16 w-full mt-4 flex-col gap-4 p-4 overflow-y-scroll">
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
<p> hi hi from the overview page </p>
</div>
</main>
</>
);
}
+25
View File
@@ -0,0 +1,25 @@
import React from "react";
import { IconSvgProps } from "../types/index";
export const AcmeIcon: React.FC<IconSvgProps> = ({
size = 32,
width,
height,
...props
}) => (
<svg
fill="none"
height={size || height}
viewBox="0 0 32 32"
width={size || width}
{...props}
>
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
View File
+327
View File
@@ -0,0 +1,327 @@
"use client";
import { Icon } from "@iconify/react";
import {
Accordion,
AccordionItem,
type ListboxProps,
type ListboxSectionProps,
type Selection,
} from "@nextui-org/react";
import {
Listbox,
ListboxItem,
ListboxSection,
Tooltip,
} from "@nextui-org/react";
import React from "react";
import { cn } from "@/utils/cn";
export enum SidebarItemType {
Nest = "nest",
}
export type SidebarItem = {
key: string;
title: string;
icon?: string;
href?: string;
type?: SidebarItemType.Nest;
startContent?: React.ReactNode;
endContent?: React.ReactNode;
items?: SidebarItem[];
className?: string;
};
export type SidebarProps = Omit<ListboxProps<SidebarItem>, "children"> & {
items: SidebarItem[];
isCompact?: boolean;
hideEndContent?: boolean;
iconClassName?: string;
sectionClasses?: ListboxSectionProps["classNames"];
classNames?: ListboxProps["classNames"];
defaultSelectedKey: string;
onSelect?: (key: string) => void;
};
const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
(
{
items,
isCompact,
defaultSelectedKey,
onSelect,
hideEndContent,
sectionClasses: sectionClassesProp = {},
itemClasses: itemClassesProp = {},
iconClassName,
classNames,
className,
...props
},
ref,
) => {
const [selected, setSelected] =
React.useState<React.Key>(defaultSelectedKey);
const sectionClasses = {
...sectionClassesProp,
base: cn(sectionClassesProp?.base, "w-full", {
"p-0 max-w-[44px]": isCompact,
}),
group: cn(sectionClassesProp?.group, {
"flex flex-col gap-1": isCompact,
}),
heading: cn(sectionClassesProp?.heading, {
hidden: isCompact,
}),
};
const itemClasses = {
...itemClassesProp,
base: cn(itemClassesProp?.base, {
"w-11 h-11 gap-0 p-0": isCompact,
}),
};
const renderNestItem = React.useCallback(
(item: SidebarItem) => {
const isNestType =
item.items &&
item.items?.length > 0 &&
item?.type === SidebarItemType.Nest;
if (isNestType) {
// Is a nest type item , so we need to remove the href
delete item.href;
}
return (
<ListboxItem
{...item}
key={item.key}
classNames={{
base: cn(
{
"h-auto p-0": !isCompact && isNestType,
},
{
"inline-block w-11": isCompact && isNestType,
},
),
}}
endContent={
isCompact || isNestType || hideEndContent
? null
: item.endContent ?? null
}
startContent={
isCompact || isNestType ? null : item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)
}
title={isCompact || isNestType ? null : item.title}
>
{isCompact ? (
<Tooltip content={item.title} placement="right">
<div className="flex w-full items-center justify-center">
{item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)}
</div>
</Tooltip>
) : null}
{!isCompact && isNestType ? (
<Accordion className={"p-0"}>
<AccordionItem
key={item.key}
aria-label={item.title}
classNames={{
heading: "pr-3",
trigger: "p-0",
content: "py-0 pl-4",
}}
title={
item.icon ? (
<div
className={"flex h-11 items-center gap-2 px-2 py-1.5"}
>
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
<span className="text-small font-medium text-default-500 group-data-[selected=true]:text-foreground">
{item.title}
</span>
</div>
) : (
item.startContent ?? null
)
}
>
{item.items && item.items?.length > 0 ? (
<Listbox
className={"mt-0.5"}
classNames={{
list: cn("border-l border-default-200 pl-4"),
}}
items={item.items}
variant="flat"
>
{item.items.map(renderItem)}
</Listbox>
) : (
renderItem(item)
)}
</AccordionItem>
</Accordion>
) : null}
</ListboxItem>
);
},
[isCompact, hideEndContent, iconClassName, items],
);
const renderItem = React.useCallback(
(item: SidebarItem) => {
const isNestType =
item.items &&
item.items?.length > 0 &&
item?.type === SidebarItemType.Nest;
if (isNestType) {
return renderNestItem(item);
}
return (
<ListboxItem
{...item}
key={item.key}
endContent={
isCompact || hideEndContent ? null : item.endContent ?? null
}
startContent={
isCompact ? null : item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)
}
textValue={item.title}
title={isCompact ? null : item.title}
>
{isCompact ? (
<Tooltip content={item.title} placement="right">
<div className="flex w-full items-center justify-center">
{item.icon ? (
<Icon
className={cn(
"text-default-500 group-data-[selected=true]:text-foreground",
iconClassName,
)}
icon={item.icon}
width={24}
/>
) : (
item.startContent ?? null
)}
</div>
</Tooltip>
) : null}
</ListboxItem>
);
},
[isCompact, hideEndContent, iconClassName, itemClasses?.base],
);
return (
<Listbox
key={isCompact ? "compact" : "default"}
ref={ref}
hideSelectedIcon
as="nav"
className={cn("list-none", className)}
classNames={{
...classNames,
list: cn("items-center", classNames?.list),
}}
color="default"
itemClasses={{
...itemClasses,
base: cn(
"px-3 min-h-11 rounded-large h-[44px] data-[selected=true]:bg-default-100",
itemClasses?.base,
),
title: cn(
"text-small font-medium text-default-500 group-data-[selected=true]:text-foreground",
itemClasses?.title,
),
}}
items={items}
selectedKeys={[selected] as unknown as Selection}
selectionMode="single"
variant="flat"
onSelectionChange={(keys) => {
const key = Array.from(keys)[0];
setSelected(key as React.Key);
onSelect?.(key as string);
}}
{...props}
>
{(item) => {
return item.items &&
item.items?.length > 0 &&
item?.type === SidebarItemType.Nest ? (
renderNestItem(item)
) : item.items && item.items?.length > 0 ? (
<ListboxSection
key={item.key}
classNames={sectionClasses}
showDivider={isCompact}
title={item.title}
>
{item.items.map(renderItem)}
</ListboxSection>
) : (
renderItem(item)
);
}}
</Listbox>
);
},
);
Sidebar.displayName = "Sidebar";
export default Sidebar;
+503
View File
@@ -0,0 +1,503 @@
import { Icon } from "@iconify/react";
import { Chip } from "@nextui-org/react";
import { SidebarItem, SidebarItemType } from "./Sidebar";
import TeamAvatar from "./TeamAvatar";
/**
* Please check the https://nextui.org/docs/guide/routing to have a seamless router integration
*/
export const items: SidebarItem[] = [
{
key: "home",
href: "#",
icon: "solar:home-2-linear",
title: "Home",
},
{
key: "projects",
href: "#",
icon: "solar:widget-2-outline",
title: "Projects",
endContent: (
<Icon
className="text-default-400"
icon="solar:add-circle-line-duotone"
width={24}
/>
),
},
{
key: "tasks",
href: "#",
icon: "solar:checklist-minimalistic-outline",
title: "Tasks",
endContent: (
<Icon
className="text-default-400"
icon="solar:add-circle-line-duotone"
width={24}
/>
),
},
{
key: "team",
href: "#",
icon: "solar:users-group-two-rounded-outline",
title: "Team",
},
{
key: "tracker",
href: "#",
icon: "solar:sort-by-time-linear",
title: "Tracker",
endContent: (
<Chip size="sm" variant="flat">
New
</Chip>
),
},
{
key: "analytics",
href: "#",
icon: "solar:chart-outline",
title: "Analytics",
},
{
key: "perks",
href: "#",
icon: "solar:gift-linear",
title: "Perks",
endContent: (
<Chip size="sm" variant="flat">
3
</Chip>
),
},
{
key: "expenses",
href: "#",
icon: "solar:bill-list-outline",
title: "Expenses",
},
{
key: "settings",
href: "#",
icon: "solar:settings-outline",
title: "Settings",
},
];
export const sectionItems: SidebarItem[] = [
{
key: "dashboards",
title: "Dashboards",
items: [
{
key: "home",
href: "/",
icon: "solar:home-2-linear",
title: "Overview",
},
// {
// key: "projects",
// href: "#",
// icon: "solar:widget-2-outline",
// title: "Projects",
// endContent: (
// <Icon
// className="text-default-400"
// icon="solar:add-circle-line-duotone"
// width={24}
// />
// ),
// },
// {
// key: "tasks",
// href: "#",
// icon: "solar:checklist-minimalistic-outline",
// title: "Tasks",
// endContent: (
// <Icon
// className="text-default-400"
// icon="solar:add-circle-line-duotone"
// width={24}
// />
// ),
// },
{
key: "services",
href: "/services",
icon: "solar:users-group-two-rounded-outline",
title: "Services",
},
{
key: "compliance",
href: "/compliance",
icon: "solar:sort-by-time-linear",
title: "Compliance",
endContent: (
<Chip size="sm" variant="flat">
New
</Chip>
),
},
],
},
{
key: "scan",
title: "Scan",
items: [
{
key: "findings",
href: "/findings",
title: "Findings",
icon: "solar:pie-chart-2-outline",
items: [
{
key: "shareholders",
href: "#",
title: "Shareholders",
},
{
key: "note_holders",
href: "#",
title: "Note Holders",
},
{
key: "transactions_log",
href: "#",
title: "Transactions Log",
},
],
},
],
},
{
key: "accounts",
title: "Accounts",
items: [
{
key: "cloud",
href: "/cloud",
icon: "solar:gift-linear",
title: "Cloud",
endContent: (
<Chip size="sm" variant="flat">
3
</Chip>
),
},
{
key: "integration",
href: "/integration",
icon: "solar:bill-list-outline",
title: "Integration",
},
{
key: "settings",
href: "/settings",
icon: "solar:settings-outline",
title: "Settings",
},
],
},
];
export const sectionItemsWithTeams: SidebarItem[] = [
...sectionItems,
{
key: "team",
title: "Team",
items: [
{
key: "users",
href: "#",
title: "Users",
startContent: <TeamAvatar name="Users page" />,
},
{
key: "roles",
href: "#",
title: "Roles",
startContent: <TeamAvatar name="Roles page" />,
},
],
},
];
export const brandItems: SidebarItem[] = [
{
key: "overview",
title: "Overview",
items: [
{
key: "home",
href: "/",
icon: "solar:home-2-linear",
title: "Home",
},
{
key: "projects",
href: "#",
icon: "solar:widget-2-outline",
title: "Projects",
endContent: (
<Icon
className="text-primary-foreground/60"
icon="solar:add-circle-line-duotone"
width={24}
/>
),
},
{
key: "tasks",
href: "#",
icon: "solar:checklist-minimalistic-outline",
title: "Tasks",
endContent: (
<Icon
className="text-primary-foreground/60"
icon="solar:add-circle-line-duotone"
width={24}
/>
),
},
{
key: "team",
href: "#",
icon: "solar:users-group-two-rounded-outline",
title: "Team",
},
{
key: "tracker",
href: "#",
icon: "solar:sort-by-time-linear",
title: "Tracker",
endContent: (
<Chip
className="bg-primary-foreground font-medium text-primary"
size="sm"
variant="flat"
>
New
</Chip>
),
},
],
},
{
key: "your-teams",
title: "Your Teams",
items: [
{
key: "nextui",
href: "#",
title: "NextUI",
startContent: (
<TeamAvatar
classNames={{
base: "border-1 border-primary-foreground/20",
name: "text-primary-foreground/80",
}}
name="Next UI"
/>
),
},
{
key: "tailwind-variants",
href: "#",
title: "Tailwind Variants",
startContent: (
<TeamAvatar
classNames={{
base: "border-1 border-primary-foreground/20",
name: "text-primary-foreground/80",
}}
name="Tailwind Variants"
/>
),
},
{
key: "nextui-pro",
href: "#",
title: "NextUI Pro",
startContent: (
<TeamAvatar
classNames={{
base: "border-1 border-primary-foreground/20",
name: "text-primary-foreground/80",
}}
name="NextUI Pro"
/>
),
},
],
},
];
export const sectionLongList: SidebarItem[] = [
...sectionItems,
{
key: "payments",
title: "Payments",
items: [
{
key: "payroll",
href: "#",
title: "Payroll",
icon: "solar:dollar-minimalistic-linear",
},
{
key: "invoices",
href: "#",
title: "Invoices",
icon: "solar:file-text-linear",
},
{
key: "billing",
href: "#",
title: "Billing",
icon: "solar:card-outline",
},
{
key: "payment-methods",
href: "#",
title: "Payment Methods",
icon: "solar:wallet-money-outline",
},
{
key: "payouts",
href: "#",
title: "Payouts",
icon: "solar:card-transfer-outline",
},
],
},
{
key: "your-teams",
title: "Your Teams",
items: [
{
key: "nextui",
href: "#",
title: "NextUI",
startContent: <TeamAvatar name="Next UI" />,
},
{
key: "tailwind-variants",
href: "#",
title: "Tailwind Variants",
startContent: <TeamAvatar name="Tailwind Variants" />,
},
{
key: "nextui-pro",
href: "#",
title: "NextUI Pro",
startContent: <TeamAvatar name="NextUI Pro" />,
},
],
},
];
export const sectionNestedItems: SidebarItem[] = [
{
key: "home",
href: "#",
icon: "solar:home-2-linear",
title: "Home",
},
{
key: "projects",
href: "#",
icon: "solar:widget-2-outline",
title: "Projects",
endContent: (
<Icon
className="text-default-400"
icon="solar:add-circle-line-duotone"
width={24}
/>
),
},
{
key: "tasks",
href: "#",
icon: "solar:checklist-minimalistic-outline",
title: "Tasks",
endContent: (
<Icon
className="text-default-400"
icon="solar:add-circle-line-duotone"
width={24}
/>
),
},
{
key: "team",
href: "#",
icon: "solar:users-group-two-rounded-outline",
title: "Team",
},
{
key: "tracker",
href: "#",
icon: "solar:sort-by-time-linear",
title: "Tracker",
endContent: (
<Chip size="sm" variant="flat">
New
</Chip>
),
},
{
key: "analytics",
href: "#",
icon: "solar:chart-outline",
title: "Analytics",
},
{
key: "perks",
href: "#",
icon: "solar:gift-linear",
title: "Perks",
endContent: (
<Chip size="sm" variant="flat">
3
</Chip>
),
},
{
key: "cap_table",
title: "Cap Table",
icon: "solar:pie-chart-2-outline",
type: SidebarItemType.Nest,
items: [
{
key: "shareholders",
icon: "solar:users-group-rounded-linear",
href: "#",
title: "Shareholders",
},
{
key: "note_holders",
icon: "solar:notes-outline",
href: "#",
title: "Note Holders",
},
{
key: "transactions_log",
icon: "solar:clipboard-list-linear",
href: "#",
title: "Transactions Log",
},
],
},
{
key: "expenses",
href: "#",
icon: "solar:bill-list-outline",
title: "Expenses",
},
];
+191
View File
@@ -0,0 +1,191 @@
"use client";
import { Icon } from "@iconify/react";
import { Button, ScrollShadow, Spacer, Tooltip } from "@nextui-org/react";
import React from "react";
import { useMediaQuery } from "usehooks-ts";
import { cn } from "@/utils/cn";
import { AcmeIcon } from "../acme";
import { ThemeSwitch } from "../ThemeSwitch";
import Sidebar from "./Sidebar";
import { sectionItemsWithTeams } from "./SidebarItems";
import UserAvatar from "./UserAvatar";
export const SidebarWrap = () => {
const [isCollapsed, setIsCollapsed] = React.useState(false);
const isMobile = useMediaQuery("(max-width: 768px)");
const isCompact = isCollapsed || isMobile;
const onToggle = React.useCallback(() => {
setIsCollapsed((prev) => !prev);
}, []);
return (
<div
className={cn(
"relative flex h-screen w-72 flex-col !border-r-small border-divider p-6 transition-width",
{
"w-16 items-center px-2 py-6": isCompact,
},
)}
>
<div
className={cn(
"flex items-center gap-3 px-3",
{
"justify-center gap-0": isCompact,
},
)}
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-foreground">
<AcmeIcon className="text-background" />
</div>
<span
className={cn("text-small font-bold uppercase opacity-100", {
"w-0 opacity-0": isCompact,
})}
>
Prowler
</span>
</div>
<Spacer y={8} />
<UserAvatar
userName={"Pablo Lara"}
position={"Software Engineer"}
isCompact={isCompact}
/>
<ScrollShadow className="-mr-6 h-full max-h-full py-6 pr-6">
<Sidebar
defaultSelectedKey="home"
isCompact={isCompact}
items={sectionItemsWithTeams}
/>
</ScrollShadow>
<Spacer y={2} />
<div
className={cn("mt-auto flex flex-col", {
"items-center": isCompact,
})}
>
<Tooltip
content="Help & Feedback"
isDisabled={!isCompact}
placement="right"
>
<Button
fullWidth
className={cn(
"justify-start truncate text-default-500 data-[hover=true]:text-foreground",
{
"justify-center": isCompact,
},
)}
isIconOnly={isCompact}
startContent={
isCompact ? null : (
<Icon
className="flex-none text-default-500"
icon="solar:info-circle-line-duotone"
width={24}
/>
)
}
variant="light"
>
{isCompact ? (
<Icon
className="text-default-500"
icon="solar:info-circle-line-duotone"
width={24}
/>
) : (
"Help & Information"
)}
</Button>
</Tooltip>
<Tooltip content="Log Out" isDisabled={!isCompact} placement="right">
<Button
className={cn(
"justify-start text-default-500 data-[hover=true]:text-foreground",
{
"justify-center": isCompact,
},
)}
isIconOnly={isCompact}
startContent={
isCompact ? null : (
<Icon
className="flex-none rotate-180 text-default-500"
icon="solar:minus-circle-line-duotone"
width={24}
/>
)
}
variant="light"
>
{isCompact ? (
<Icon
className="rotate-180 text-default-500"
icon="solar:minus-circle-line-duotone"
width={24}
/>
) : (
"Log Out"
)}
</Button>
</Tooltip>
</div>
<div
className={cn("mt-auto flex justify-end gap-3 items-baseline", {
"flex-col items-center": isCompact,
})}
>
<Tooltip
content="Light | Dark mode"
placement={isCompact ? "right" : "top"}
>
<div
className={cn(
"text-default-500 data-[hover=true]:text-foreground px-0",
{
"justify-center mt-3": isCompact,
},
)}
>
<ThemeSwitch />
</div>
</Tooltip>
<Tooltip
content="Open | Close sidebar"
placement={isCompact ? "right" : "top"}
>
<Button
className={cn(
"text-default-500 data-[hover=true]:text-foreground px-0",
{
"justify-center": isCompact,
},
)}
isIconOnly
size="sm"
variant="light"
onPress={onToggle}
>
<Icon
className="text-default-500"
height={24}
icon="solar:sidebar-minimalistic-outline"
width={24}
/>
</Button>
</Tooltip>
</div>
</div>
);
};
+38
View File
@@ -0,0 +1,38 @@
"use client";
import type { AvatarProps } from "@nextui-org/react";
import { Avatar } from "@nextui-org/react";
import React from "react";
import { cn } from "@/utils/cn";
const TeamAvatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
({ name, className, classNames = {}, ...props }, ref) => (
<Avatar
{...props}
ref={ref}
classNames={{
...classNames,
base: cn(
"bg-transparent border border-divider",
classNames?.base,
className,
),
name: cn(
"text-default-500 text-[0.6rem] font-semibold",
classNames?.name,
),
}}
getInitials={(name) =>
(name[0] || "") + (name[name.lastIndexOf(" ") + 1] || "").toUpperCase()
}
name={name}
radius="md"
size="sm"
/>
),
);
TeamAvatar.displayName = "TeamAvatar";
export default TeamAvatar;
+35
View File
@@ -0,0 +1,35 @@
"use client";
import { Avatar } from "@nextui-org/react";
import React from "react";
import { cn } from "@/utils/cn";
interface UserAvatarProps {
userName: string;
position: string;
isCompact: boolean;
}
const UserAvatar: React.FC<UserAvatarProps> = ({
userName,
position,
isCompact = false,
}) => {
return (
<div className="flex items-center gap-3 px-3">
<Avatar isBordered className="flex-none" size="sm" showFallback />
<div
className={cn("flex max-w-full flex-col", {
hidden: isCompact,
})}
>
<p className="truncate text-small font-medium text-default-600">
{userName}
</p>
<p className="truncate text-tiny text-default-400">{position}</p>
</div>
</div>
);
};
export default UserAvatar;
+3 -1
View File
@@ -16,6 +16,7 @@
"swr": "^2.2.5"
},
"devDependencies": {
"@iconify/react": "^5.0.1",
"@types/node": "20.5.7",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
@@ -39,7 +40,8 @@
"postcss": "8.4.38",
"tailwind-variants": "0.1.20",
"tailwindcss": "3.4.3",
"typescript": "5.0.4"
"typescript": "5.0.4",
"usehooks-ts": "^3.1.0"
},
"name": "prowler-next-app",
"private": true,
+8 -5
View File
@@ -1,11 +1,11 @@
import {nextui} from '@nextui-org/theme'
import { nextui } from "@nextui-org/theme";
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
@@ -13,8 +13,11 @@ module.exports = {
sans: ["var(--font-sans)"],
mono: ["var(--font-geist-mono)"],
},
height: {
"dvh-minus-16": "calc(100dvh - 116px)",
},
},
},
darkMode: "class",
plugins: [nextui()],
}
};
+22
View File
@@ -0,0 +1,22 @@
import type { ClassValue } from "clsx";
import clsx from "clsx";
import { twMerge } from "tailwind-merge";
// Definition of custom classes you want to combine
const customClasses = new Map<string, string>([
["text-small", "text-small"],
["text-default-500", "text-default-500"],
// Add more custom classes as needed
]);
export function cn(...inputs: ClassValue[]) {
// Filter and combine custom classes before passing them to twMerge
const filteredInputs = inputs.map((input) => {
if (typeof input === "string") {
return customClasses.get(input) || input;
}
return input;
});
return twMerge(clsx(filteredInputs));
}