From b63d1f1292da5822ad52cf1641a2ff6fd8f5aecc Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 8 Jul 2024 10:20:39 +0200 Subject: [PATCH 01/11] feat: add eslint-plugin-security --- .eslintrc.cjs | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1843e62bcf..902ee59d19 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -9,6 +9,7 @@ module.exports = { "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", + "plugin:security/recommended-legacy", "prettier", ], parserOptions: { diff --git a/package.json b/package.json index 611c3b9137..6a8e780241 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "devDependencies": { "eslint-config-prettier": "^9.1.0", + "eslint-plugin-security": "^3.0.1", "eslint-plugin-simple-import-sort": "^12.1.1" }, "name": "prowler-next-app", From e10bb9e3f2d686cab2615a3a245771d1c27ae685 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 8 Jul 2024 10:59:43 +0200 Subject: [PATCH 02/11] chore: relocate devDependencies to the appropriate section in package.json --- package.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 6a8e780241..a47863db4e 100644 --- a/package.json +++ b/package.json @@ -6,39 +6,39 @@ "@nextui-org/theme": "2.2.5", "@react-aria/ssr": "3.9.4", "@react-aria/visually-hidden": "3.8.12", + "clsx": "2.1.1", + "framer-motion": "~11.1.1", + "intl-messageformat": "^10.5.0", + "next": "14.2.3", + "next-themes": "^0.2.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "swr": "^2.2.5" + }, + "devDependencies": { "@types/node": "20.5.7", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@typescript-eslint/eslint-plugin": "^7.10.0", "@typescript-eslint/parser": "^7.10.0", "autoprefixer": "10.4.19", - "clsx": "2.1.1", "eslint": "^8.56.0", "eslint-config-next": "14.2.1", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.23.2", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-security": "^3.0.1", + "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^3.2.0", - "framer-motion": "~11.1.1", - "intl-messageformat": "^10.5.0", - "next": "14.2.3", - "next-themes": "^0.2.1", "postcss": "8.4.38", - "react": "18.3.1", - "react-dom": "18.3.1", - "swr": "^2.2.5", "tailwind-variants": "0.1.20", "tailwindcss": "3.4.3", "typescript": "5.0.4" }, - "devDependencies": { - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-security": "^3.0.1", - "eslint-plugin-simple-import-sort": "^12.1.1" - }, "name": "prowler-next-app", "private": true, "scripts": { From 2337b203d0eb91d482543f8a67cad6edc44a1c83 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 8 Jul 2024 11:12:57 +0200 Subject: [PATCH 03/11] feat: add husky library for pre-commit hooks --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a47863db4e..c7ba81e435 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "eslint-plugin-security": "^3.0.1", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^3.2.0", + "husky": "^9.0.11", + "lint-staged": "^15.2.7", "postcss": "8.4.38", "tailwind-variants": "0.1.20", "tailwindcss": "3.4.3", @@ -50,7 +52,8 @@ "lint:check": "./node_modules/.bin/eslint ./app", "lint:fix": "eslint . --ext .ts,.tsx -c .eslintrc.cjs --fix", "format:check": "./node_modules/.bin/prettier --check ./app", - "format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app" + "format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app", + "prepare": "husky" }, "version": "0.0.1" } From f7c7b6a5ba3f624da0fa27701fb0550ff9dcd5f9 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 8 Jul 2024 11:26:26 +0200 Subject: [PATCH 04/11] feat: add husky library for pre-commit hooks --- .husky/pre-commit | 1 + 1 file changed, 1 insertion(+) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000000..129b71298f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run healthcheck From cf49641d5cba8f7db23101ceccd9be2464c77d65 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 9 Jul 2024 12:44:25 +0200 Subject: [PATCH 05/11] chore: improve prettierrc config --- .prettierrc.json | 4 ++-- .vscode/settings.json | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index b355961f7d..15d19325bb 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -5,5 +5,5 @@ "tabWidth": 2, "useTabs": false, "semi": true, - "printWidth": 120 -} \ No newline at end of file + "printWidth": 80 +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215fd..104c678edb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,11 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.printWidth": 80, + "prettier.tabWidth": 2, + "prettier.useTabs": false, + "prettier.singleQuote": false, + "prettier.trailingComma": "all", + "prettier.semi": true, + "prettier.bracketSpacing": true } From 6138c7da9d70329cdafa6d24f5ca795c54ac54fb Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 9 Jul 2024 13:04:04 +0200 Subject: [PATCH 06/11] chore: update prettier config and fix linting issues --- app/about/layout.tsx | 10 +- app/blog/layout.tsx | 10 +- app/clouds/layout.tsx | 6 +- app/clouds/page.tsx | 22 ++++- app/docs/layout.tsx | 10 +- app/error.tsx | 8 +- app/layout.tsx | 19 +++- app/login/layout.tsx | 10 +- app/login/page.tsx | 23 ++++- app/page.tsx | 28 ++++-- app/pricing/layout.tsx | 10 +- .../{theme-switch.tsx => ThemeSwitch.tsx} | 29 +++++- components/icons.tsx | 95 ++++++++++++++++--- components/navbar.tsx | 4 +- components/primitives.ts | 10 +- 15 files changed, 244 insertions(+), 50 deletions(-) rename components/{theme-switch.tsx => ThemeSwitch.tsx} (70%) diff --git a/app/about/layout.tsx b/app/about/layout.tsx index cfdc03b801..98956a52ad 100644 --- a/app/about/layout.tsx +++ b/app/about/layout.tsx @@ -1,7 +1,13 @@ -export default function AboutLayout({ children }: { children: React.ReactNode }) { +export default function AboutLayout({ + children, +}: { + children: React.ReactNode; +}) { return (
-
{children}
+
+ {children} +
); } diff --git a/app/blog/layout.tsx b/app/blog/layout.tsx index 365caee773..911d0bcf41 100644 --- a/app/blog/layout.tsx +++ b/app/blog/layout.tsx @@ -1,7 +1,13 @@ -export default function BlogLayout({ children }: { children: React.ReactNode }) { +export default function BlogLayout({ + children, +}: { + children: React.ReactNode; +}) { return (
-
{children}
+
+ {children} +
); } diff --git a/app/clouds/layout.tsx b/app/clouds/layout.tsx index 8c8daab55d..d9ca1e80e4 100644 --- a/app/clouds/layout.tsx +++ b/app/clouds/layout.tsx @@ -1,4 +1,8 @@ -export default function CloudsLayout({ children }: { children: React.ReactNode }) { +export default function CloudsLayout({ + children, +}: { + children: React.ReactNode; +}) { return (
{children}
diff --git a/app/clouds/page.tsx b/app/clouds/page.tsx index 48d18551f8..d546612378 100644 --- a/app/clouds/page.tsx +++ b/app/clouds/page.tsx @@ -22,13 +22,21 @@ import { title } from "@/components/primitives"; import { fetcher } from "@/utils/fetcher"; export default function CloudsPage() { - const getAccounts = useSWR("http://localhost:8080/api/v1/providers/aws/accounts", fetcher); + const getAccounts = useSWR( + "http://localhost:8080/api/v1/providers/aws/accounts", + fetcher, + ); - const getAudits = useSWR("http://localhost:8080/api/v1/providers/aws/audits", fetcher); + const getAudits = useSWR( + "http://localhost:8080/api/v1/providers/aws/audits", + fetcher, + ); // TODO FIX TYPE CHECKING const getScanDetails = (account_id: number, detail: string) => { - const scan = getAudits.data && getAudits.data.find((audit: any) => audit.account_id === account_id); + const scan = + getAudits.data && + getAudits.data.find((audit: any) => audit.account_id === account_id); if (detail === "status") { return scan?.audit_complete && "Completed"; @@ -70,8 +78,12 @@ export default function CloudsPage() {

Cloud Accounts

- {getAccounts.error && Failed to load} - {getAccounts.isLoading && Loading} + {getAccounts.error && ( + Failed to load + )} + {getAccounts.isLoading && ( + Loading + )}

{getAccounts.data && ( diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx index e3d89f490a..eaf63f3b99 100644 --- a/app/docs/layout.tsx +++ b/app/docs/layout.tsx @@ -1,7 +1,13 @@ -export default function DocsLayout({ children }: { children: React.ReactNode }) { +export default function DocsLayout({ + children, +}: { + children: React.ReactNode; +}) { return (
-
{children}
+
+ {children} +
); } diff --git a/app/error.tsx b/app/error.tsx index 388b7fcc7b..9ed5104e8a 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -2,7 +2,13 @@ import { useEffect } from "react"; -export default function Error({ error, reset }: { error: Error; reset: () => void }) { +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { useEffect(() => { // Log the error to an error reporting service /* eslint-disable no-console */ diff --git a/app/layout.tsx b/app/layout.tsx index 39517f2bf1..275ebe292b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,9 +1,9 @@ import "@/styles/globals.css"; +import { Navbar } from "@nextui-org/react"; import clsx from "clsx"; import { Metadata, Viewport } from "next"; -import { Navbar } from "@/components/navbar"; import { fontSans } from "@/config/fonts"; import { siteConfig } from "@/config/site"; @@ -27,15 +27,26 @@ export const viewport: Viewport = { ], }; -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { return ( - +
-
{children}
+
+ {children} +
diff --git a/app/login/layout.tsx b/app/login/layout.tsx index 561d6bcc49..d3a4a58ea4 100644 --- a/app/login/layout.tsx +++ b/app/login/layout.tsx @@ -1,7 +1,13 @@ -export default function LoginLayout({ children }: { children: React.ReactNode }) { +export default function LoginLayout({ + children, +}: { + children: React.ReactNode; +}) { return (
-
{children}
+
+ {children} +
); } diff --git a/app/login/page.tsx b/app/login/page.tsx index c348917daf..f07535d803 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -2,7 +2,15 @@ import { EyeIcon } from "@heroicons/react/24/solid"; import { EyeSlashIcon } from "@heroicons/react/24/solid"; -import { Button, Card, CardBody, CardFooter, CardHeader, Input, Link } from "@nextui-org/react"; +import { + Button, + Card, + CardBody, + CardFooter, + CardHeader, + Input, + Link, +} from "@nextui-org/react"; import React from "react"; export default function LoginPage() { @@ -18,13 +26,22 @@ export default function LoginPage() {

Login

- + + - ); -}; diff --git a/components/icons.tsx b/components/temp_Icons.tsx similarity index 100% rename from components/icons.tsx rename to components/temp_Icons.tsx diff --git a/components/navbar.tsx b/components/temp_Navbar.tsx similarity index 100% rename from components/navbar.tsx rename to components/temp_Navbar.tsx From 4d43a6bdd6349498e82705a8e3d6df5cdefcc5e3 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 9 Jul 2024 13:43:49 +0200 Subject: [PATCH 09/11] chore: rename temp_files.tsx to fix case sensitivity issue on GitHub --- components/{temp_Icons.tsx => Icons.tsx} | 0 components/{temp_Navbar.tsx => Navbar.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename components/{temp_Icons.tsx => Icons.tsx} (100%) rename components/{temp_Navbar.tsx => Navbar.tsx} (100%) diff --git a/components/temp_Icons.tsx b/components/Icons.tsx similarity index 100% rename from components/temp_Icons.tsx rename to components/Icons.tsx diff --git a/components/temp_Navbar.tsx b/components/Navbar.tsx similarity index 100% rename from components/temp_Navbar.tsx rename to components/Navbar.tsx From 746b427943851a6bfdaaeafe6c0e65d412e28909 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 11 Jul 2024 10:31:06 +0200 Subject: [PATCH 10/11] feat: set basic sidebar and the main layout --- app/layout.tsx | 13 +- app/page.tsx | 224 +++++++++---- components/acme.tsx | 25 ++ components/sidebar.tsx | 0 components/sidebar/Sidebar.tsx | 327 ++++++++++++++++++ components/sidebar/SidebarItems.tsx | 503 ++++++++++++++++++++++++++++ components/sidebar/SidebarWrap.tsx | 191 +++++++++++ components/sidebar/TeamAvatar.tsx | 38 +++ components/sidebar/UserAvatar.tsx | 35 ++ package.json | 4 +- tailwind.config.js | 13 +- utils/cn.ts | 22 ++ 12 files changed, 1327 insertions(+), 68 deletions(-) create mode 100644 components/acme.tsx delete mode 100644 components/sidebar.tsx create mode 100644 components/sidebar/Sidebar.tsx create mode 100644 components/sidebar/SidebarItems.tsx create mode 100644 components/sidebar/SidebarWrap.tsx create mode 100644 components/sidebar/TeamAvatar.tsx create mode 100644 components/sidebar/UserAvatar.tsx create mode 100644 utils/cn.ts 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)); +} From f31a92ea984a1f2d304a88b753856e9960e2b4ee Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 11 Jul 2024 15:31:24 +0200 Subject: [PATCH 11/11] feat(sidebar): add state persistence using localStorage --- app/layout.tsx | 8 +++----- components/sidebar/SidebarWrap.tsx | 24 ++++++++++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/layout.tsx b/app/layout.tsx index 074a962755..c414a0bf59 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -43,11 +43,9 @@ export default function RootLayout({ )} > -
-
- -
{children}
-
+
+ +
{children}
diff --git a/components/sidebar/SidebarWrap.tsx b/components/sidebar/SidebarWrap.tsx index 533b1819fc..81f2e195aa 100644 --- a/components/sidebar/SidebarWrap.tsx +++ b/components/sidebar/SidebarWrap.tsx @@ -2,7 +2,7 @@ import { Icon } from "@iconify/react"; import { Button, ScrollShadow, Spacer, Tooltip } from "@nextui-org/react"; -import React from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useMediaQuery } from "usehooks-ts"; import { cn } from "@/utils/cn"; @@ -14,15 +14,31 @@ import { sectionItemsWithTeams } from "./SidebarItems"; import UserAvatar from "./UserAvatar"; export const SidebarWrap = () => { - const [isCollapsed, setIsCollapsed] = React.useState(false); + const [isCollapsed, setIsCollapsed] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const isMobile = useMediaQuery("(max-width: 768px)"); const isCompact = isCollapsed || isMobile; - const onToggle = React.useCallback(() => { - setIsCollapsed((prev) => !prev); + useEffect(() => { + const savedState = localStorage.getItem("isCollapsed"); + if (savedState !== null) { + setIsCollapsed(JSON.parse(savedState)); + } + setIsLoaded(true); }, []); + const onToggle = useCallback(() => { + setIsCollapsed((prev) => { + const newState = !prev; + localStorage.setItem("isCollapsed", JSON.stringify(newState)); + return newState; + }); + }, []); + + if (!isLoaded) return null; + return (