diff --git a/app/layout.tsx b/app/layout.tsx index 275ebe292b..074a962755 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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({ )} > -
- -
- {children} -
+
+
+ +
{children}
+
diff --git a/app/page.tsx b/app/page.tsx index ceb0e6a542..268ac3a2ea 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- - -

Login

-
- - +
+
+ - } - type={isVisible ? "text" : "password"} - className="max-w-xs" - /> - - - - - -

This is a page with "use client", useState

-
-
+ +

Overview

+ +
+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+

hi hi from the overview page

+
+ + ); } diff --git a/components/acme.tsx b/components/acme.tsx new file mode 100644 index 0000000000..9478723fa5 --- /dev/null +++ b/components/acme.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +import { IconSvgProps } from "../types/index"; + +export const AcmeIcon: React.FC = ({ + size = 32, + width, + height, + ...props +}) => ( + + + +); diff --git a/components/sidebar.tsx b/components/sidebar.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/components/sidebar/Sidebar.tsx b/components/sidebar/Sidebar.tsx new file mode 100644 index 0000000000..49df2deac7 --- /dev/null +++ b/components/sidebar/Sidebar.tsx @@ -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, "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( + ( + { + items, + isCompact, + defaultSelectedKey, + onSelect, + hideEndContent, + sectionClasses: sectionClassesProp = {}, + itemClasses: itemClassesProp = {}, + iconClassName, + classNames, + className, + ...props + }, + ref, + ) => { + const [selected, setSelected] = + React.useState(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 ( + + ) : ( + item.startContent ?? null + ) + } + title={isCompact || isNestType ? null : item.title} + > + {isCompact ? ( + +
+ {item.icon ? ( + + ) : ( + item.startContent ?? null + )} +
+
+ ) : null} + {!isCompact && isNestType ? ( + + + + + {item.title} + +
+ ) : ( + item.startContent ?? null + ) + } + > + {item.items && item.items?.length > 0 ? ( + + {item.items.map(renderItem)} + + ) : ( + renderItem(item) + )} + + + ) : null} + + ); + }, + [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 ( + + ) : ( + item.startContent ?? null + ) + } + textValue={item.title} + title={isCompact ? null : item.title} + > + {isCompact ? ( + +
+ {item.icon ? ( + + ) : ( + item.startContent ?? null + )} +
+
+ ) : null} +
+ ); + }, + [isCompact, hideEndContent, iconClassName, itemClasses?.base], + ); + + return ( + { + 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 ? ( + + {item.items.map(renderItem)} + + ) : ( + renderItem(item) + ); + }} + + ); + }, +); + +Sidebar.displayName = "Sidebar"; + +export default Sidebar; diff --git a/components/sidebar/SidebarItems.tsx b/components/sidebar/SidebarItems.tsx new file mode 100644 index 0000000000..53ac6ef72f --- /dev/null +++ b/components/sidebar/SidebarItems.tsx @@ -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: ( + + ), + }, + { + key: "tasks", + href: "#", + icon: "solar:checklist-minimalistic-outline", + title: "Tasks", + endContent: ( + + ), + }, + { + key: "team", + href: "#", + icon: "solar:users-group-two-rounded-outline", + title: "Team", + }, + { + key: "tracker", + href: "#", + icon: "solar:sort-by-time-linear", + title: "Tracker", + endContent: ( + + New + + ), + }, + { + key: "analytics", + href: "#", + icon: "solar:chart-outline", + title: "Analytics", + }, + { + key: "perks", + href: "#", + icon: "solar:gift-linear", + title: "Perks", + endContent: ( + + 3 + + ), + }, + { + 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: ( + // + // ), + // }, + // { + // key: "tasks", + // href: "#", + // icon: "solar:checklist-minimalistic-outline", + // title: "Tasks", + // endContent: ( + // + // ), + // }, + { + 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: ( + + New + + ), + }, + ], + }, + { + 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: ( + + 3 + + ), + }, + { + 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: , + }, + { + key: "roles", + href: "#", + title: "Roles", + startContent: , + }, + ], + }, +]; + +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: ( + + ), + }, + { + key: "tasks", + href: "#", + icon: "solar:checklist-minimalistic-outline", + title: "Tasks", + endContent: ( + + ), + }, + { + key: "team", + href: "#", + icon: "solar:users-group-two-rounded-outline", + title: "Team", + }, + { + key: "tracker", + href: "#", + icon: "solar:sort-by-time-linear", + title: "Tracker", + endContent: ( + + New + + ), + }, + ], + }, + { + key: "your-teams", + title: "Your Teams", + items: [ + { + key: "nextui", + href: "#", + title: "NextUI", + startContent: ( + + ), + }, + { + key: "tailwind-variants", + href: "#", + title: "Tailwind Variants", + startContent: ( + + ), + }, + { + key: "nextui-pro", + href: "#", + title: "NextUI Pro", + startContent: ( + + ), + }, + ], + }, +]; + +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: , + }, + { + key: "tailwind-variants", + href: "#", + title: "Tailwind Variants", + startContent: , + }, + { + key: "nextui-pro", + href: "#", + title: "NextUI Pro", + startContent: , + }, + ], + }, +]; + +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: ( + + ), + }, + { + key: "tasks", + href: "#", + icon: "solar:checklist-minimalistic-outline", + title: "Tasks", + endContent: ( + + ), + }, + { + key: "team", + href: "#", + icon: "solar:users-group-two-rounded-outline", + title: "Team", + }, + { + key: "tracker", + href: "#", + icon: "solar:sort-by-time-linear", + title: "Tracker", + endContent: ( + + New + + ), + }, + { + key: "analytics", + href: "#", + icon: "solar:chart-outline", + title: "Analytics", + }, + { + key: "perks", + href: "#", + icon: "solar:gift-linear", + title: "Perks", + endContent: ( + + 3 + + ), + }, + { + 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", + }, +]; diff --git a/components/sidebar/SidebarWrap.tsx b/components/sidebar/SidebarWrap.tsx new file mode 100644 index 0000000000..533b1819fc --- /dev/null +++ b/components/sidebar/SidebarWrap.tsx @@ -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 ( +
+
+
+ +
+ + Prowler + +
+ + + + + + + + +
+ + + + + + +
+
+ +
+ +
+
+ + + +
+
+ ); +}; diff --git a/components/sidebar/TeamAvatar.tsx b/components/sidebar/TeamAvatar.tsx new file mode 100644 index 0000000000..e3af5cd985 --- /dev/null +++ b/components/sidebar/TeamAvatar.tsx @@ -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( + ({ name, className, classNames = {}, ...props }, ref) => ( + + (name[0] || "") + (name[name.lastIndexOf(" ") + 1] || "").toUpperCase() + } + name={name} + radius="md" + size="sm" + /> + ), +); + +TeamAvatar.displayName = "TeamAvatar"; + +export default TeamAvatar; diff --git a/components/sidebar/UserAvatar.tsx b/components/sidebar/UserAvatar.tsx new file mode 100644 index 0000000000..4f37a3155d --- /dev/null +++ b/components/sidebar/UserAvatar.tsx @@ -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 = ({ + userName, + position, + isCompact = false, +}) => { + return ( +
+ +
+

+ {userName} +

+

{position}

+
+
+ ); +}; + +export default UserAvatar; diff --git a/package.json b/package.json index c7ba81e435..3756a3d8e8 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/tailwind.config.js b/tailwind.config.js index 03a4e22e67..5a6f937328 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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()], -} +}; diff --git a/utils/cn.ts b/utils/cn.ts new file mode 100644 index 0000000000..9b89275543 --- /dev/null +++ b/utils/cn.ts @@ -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([ + ["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)); +}