diff --git a/package.json b/package.json index be8dbee..89f72bb 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,49 @@ { - "name": "dashboard-template", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@auth/prisma-adapter": "^1.0.14", - "@mantine/core": "^7.4.0", - "@mantine/form": "^7.4.0", - "@mantine/hooks": "^7.4.0", - "@prisma/client": "5.7.1", - "@tanstack/react-query": "^4.36.1", - "@tanstack/react-query-devtools": "^4.36.1", - "@trpc/client": "^10.45.0", - "@trpc/next": "^10.45.0", - "@trpc/react-query": "^10.45.0", - "@trpc/server": "^10.45.0", - "@types/bcrypt": "^5.0.2", - "@types/jsonwebtoken": "^9.0.5", - "bcrypt": "^5.1.1", - "jsonwebtoken": "^9.0.2", - "next": "14.0.4", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "superjson": "^2.2.1", - "zod": "^3.22.4" - }, - "devDependencies": { - "@types/node": "^20.10.6", - "@types/react": "^18.2.47", - "@types/react-dom": "^18.2.18", - "autoprefixer": "^10.4.16", - "eslint": "^8.56.0", - "eslint-config-next": "14.0.4", - "postcss": "^8.4.33", - "postcss-preset-mantine": "^1.12.3", - "postcss-simple-vars": "^7.0.1", - "prisma": "^5.7.1", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" - } -} + "name": "dashboard-template", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@auth/prisma-adapter": "^1.0.14", + "@mantine/core": "^7.4.0", + "@mantine/form": "^7.4.0", + "@mantine/hooks": "^7.4.0", + "@prisma/client": "5.7.1", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query-devtools": "^4.36.1", + "@trpc/client": "^10.45.0", + "@trpc/next": "^10.45.0", + "@trpc/react-query": "^10.45.0", + "@trpc/server": "^10.45.0", + "@types/bcrypt": "^5.0.2", + "@types/jsonwebtoken": "^9.0.5", + "bcrypt": "^5.1.1", + "jsonwebtoken": "^9.0.2", + "next": "14.0.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^5.0.1", + "sass": "^1.70.0", + "superjson": "^2.2.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20.10.6", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "autoprefixer": "^10.4.16", + "eslint": "^8.56.0", + "eslint-config-next": "14.0.4", + "postcss": "^8.4.33", + "postcss-preset-mantine": "^1.12.3", + "postcss-simple-vars": "^7.0.1", + "prisma": "^5.7.1", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c59a4e0..a45aa2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,13 +52,19 @@ dependencies: version: 9.0.2 next: specifier: 14.0.4 - version: 14.0.4(react-dom@18.2.0)(react@18.2.0) + version: 14.0.4(react-dom@18.2.0)(react@18.2.0)(sass@1.70.0) react: specifier: ^18.2.0 version: 18.2.0 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-icons: + specifier: ^5.0.1 + version: 5.0.1(react@18.2.0) + sass: + specifier: ^1.70.0 + version: 1.70.0 superjson: specifier: ^2.2.1 version: 2.2.1 @@ -594,7 +600,7 @@ packages: '@trpc/client': 10.45.0(@trpc/server@10.45.0) '@trpc/react-query': 10.45.0(@tanstack/react-query@4.36.1)(@trpc/client@10.45.0)(@trpc/server@10.45.0)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 10.45.0 - next: 14.0.4(react-dom@18.2.0)(react@18.2.0) + next: 14.0.4(react-dom@18.2.0)(react@18.2.0)(sass@1.70.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -798,7 +804,6 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: true /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} @@ -970,7 +975,6 @@ packages: /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - dev: true /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -989,7 +993,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /browserslist@4.22.2: resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} @@ -1055,7 +1058,6 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: true /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} @@ -1666,7 +1668,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} @@ -1722,7 +1723,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /function-bind@1.1.2: @@ -1791,7 +1791,6 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 - dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -1934,6 +1933,10 @@ packages: engines: {node: '>= 4'} dev: true + /immutable@4.3.4: + resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + dev: false + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -1997,7 +2000,6 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 - dev: true /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -2028,7 +2030,6 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true /is-finalizationregistry@1.0.2: resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} @@ -2052,7 +2053,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 - dev: true /is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -2073,7 +2073,6 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} @@ -2437,7 +2436,7 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /next@14.0.4(react-dom@18.2.0)(react@18.2.0): + /next@14.0.4(react-dom@18.2.0)(react@18.2.0)(sass@1.70.0): resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==} engines: {node: '>=18.17.0'} hasBin: true @@ -2460,6 +2459,7 @@ packages: postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + sass: 1.70.0 styled-jsx: 5.1.1(react@18.2.0) watchpack: 2.4.0 optionalDependencies: @@ -2508,7 +2508,6 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: true /normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} @@ -2674,7 +2673,6 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} @@ -2853,6 +2851,14 @@ packages: scheduler: 0.23.0 dev: false + /react-icons@5.0.1(react@18.2.0): + resolution: {integrity: sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2960,7 +2966,6 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 - dev: true /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} @@ -3056,6 +3061,16 @@ packages: is-regex: 1.1.4 dev: true + /sass@1.70.0: + resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.4 + source-map-js: 1.0.2 + dev: false + /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -3364,7 +3379,6 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} diff --git a/src/app/components/AppNavbar/Navbar.tsx b/src/app/components/AppNavbar/Navbar.tsx deleted file mode 100644 index 7f92b56..0000000 --- a/src/app/components/AppNavbar/Navbar.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { AppShell } from '@mantine/core' -import React from 'react' - -export default function AppNavbar() { - return ( - a - ) -} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 83b3d49..18c2d96 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,33 +1,21 @@ -"use client" import { AppShell, AppShellHeader, Burger } from '@mantine/core' import { useDisclosure } from '@mantine/hooks' import Image from 'next/image' import React from 'react' import logo from "@/assets/logos/logo.png" -import AppHeader from '../components/AppHeader' -import AppNavbar from '../components/AppNavbar' +import AppHeader from '../../components/AppHeader' +import AppNavbar from '../../components/AppNavbar' +import DashboardLayout from '@/components/DashboardLayout' interface Props { children: React.ReactNode } -export default function layout(props: Props) { - - const [openNavbar, { toggle }] = useDisclosure(false) +export default function Layout(props: Props) { return ( - - {/* Header */} - - - {/* Navbar */} - - - {props.children} - + + {props.children} + ) } diff --git a/src/assets/logos/logo-dsg.png b/src/assets/logos/logo-dsg.png new file mode 100644 index 0000000..c65edc6 Binary files /dev/null and b/src/assets/logos/logo-dsg.png differ diff --git a/src/app/components/AppHeader/Header.tsx b/src/components/AppHeader/Header.tsx similarity index 57% rename from src/app/components/AppHeader/Header.tsx rename to src/components/AppHeader/Header.tsx index 69973b6..7bc63a6 100644 --- a/src/app/components/AppHeader/Header.tsx +++ b/src/components/AppHeader/Header.tsx @@ -3,22 +3,25 @@ import React from 'react' import { AppShell, Burger, Group } from "@mantine/core" import { useDisclosure } from '@mantine/hooks' import Image from 'next/image' -import logo from "@/assets/logos/logo.png" +import logo from "@/assets/logos/logo-dsg.png" -export default function AppHeader() { +interface Props { + openNavbar: boolean, + toggle: () => void +} - const [openNavbar, { toggle }] = useDisclosure() +export default function AppHeader(props: Props) { return ( - + ) diff --git a/src/app/components/AppHeader/index.ts b/src/components/AppHeader/index.ts similarity index 100% rename from src/app/components/AppHeader/index.ts rename to src/components/AppHeader/index.ts diff --git a/src/components/AppNavbar/Navbar.tsx b/src/components/AppNavbar/Navbar.tsx new file mode 100644 index 0000000..6c09465 --- /dev/null +++ b/src/components/AppNavbar/Navbar.tsx @@ -0,0 +1,19 @@ +import { AppShell, ScrollArea } from '@mantine/core' +import React from 'react' +import allMenu from './_data/allMenu' +import MenuItem from './_components/MenuItem' + +export default function AppNavbar() { + + const menus = allMenu.map((menu, i) => ) + + return ( + + +
+ {menus} +
+
+
+ ) +} diff --git a/src/components/AppNavbar/_components/ChildMenu.tsx b/src/components/AppNavbar/_components/ChildMenu.tsx new file mode 100644 index 0000000..69fcf15 --- /dev/null +++ b/src/components/AppNavbar/_components/ChildMenu.tsx @@ -0,0 +1,23 @@ +import { Text } from "@mantine/core"; +import React from "react"; + +import classNames from "./childMenu.module.css"; +import { MenuItem } from "../_data/allMenu"; +import { isNotEmpty } from "@mantine/form"; + +interface Props { + item: NonNullable[number]; +} + +export default function ChildMenu(props: Props) { + return ( + + component="a" + className={classNames.link} + href={props.item.link} + onClick={(e) => e.preventDefault()} + > + {props.item.label} + + ); +} diff --git a/src/components/AppNavbar/_components/MenuItem.tsx b/src/components/AppNavbar/_components/MenuItem.tsx new file mode 100644 index 0000000..05f0bab --- /dev/null +++ b/src/components/AppNavbar/_components/MenuItem.tsx @@ -0,0 +1,71 @@ +import React, { useState } from "react"; +import { + Box, + Collapse, + Group, + ThemeIcon, + UnstyledButton, + rem, +} from "@mantine/core"; +import { MenuItem } from "../_data/allMenu"; +import { TbChevronRight } from "react-icons/tb"; + +import classNames from "./menuItem.module.css"; +import ChildMenu from "./ChildMenu"; + +interface Props { + menu: MenuItem; +} + +export default function MenuItem({ menu }: Props) { + const hasChildren = Array.isArray(menu.children); + + const [opened, setOpened] = useState(false); + + const toggleOpenMenu = () => { + setOpened((prev) => !prev); + }; + + const subItems = (hasChildren ? menu.children! : []).map((child, i) => ( + + )); + + return ( + <> + {/* Main Section */} + + + {/* Left Section */} + + {/* Icon */} + + + + + {/* Label */} + {menu.label} + + + {/* Right Section (Chevron if available) */} + {hasChildren && ( + + )} + + + {hasChildren ? {subItems} : null} + + ); +} diff --git a/src/components/AppNavbar/_components/childMenu.module.css b/src/components/AppNavbar/_components/childMenu.module.css new file mode 100644 index 0000000..43fe7a7 --- /dev/null +++ b/src/components/AppNavbar/_components/childMenu.module.css @@ -0,0 +1,16 @@ +.link { + font-weight: 500; + display: block; + text-decoration: none; + padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); + padding-left: var(--mantine-spacing-md); + margin-left: var(--mantine-spacing-xl); + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); + border-left: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + + @mixin hover { + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); + color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); + } + } \ No newline at end of file diff --git a/src/components/AppNavbar/_components/menuItem.module.css b/src/components/AppNavbar/_components/menuItem.module.css new file mode 100644 index 0000000..aab6b80 --- /dev/null +++ b/src/components/AppNavbar/_components/menuItem.module.css @@ -0,0 +1,17 @@ +.control { + font-weight: 500; + display: block; + width: 100%; + padding: var(--mantine-spacing-xs) var(--mantine-spacing-md); + color: var(--mantine-color-text); + font-size: var(--mantine-font-size-sm); + + @mixin hover { + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7)); + color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); + } + } + + .chevron { + transition: transform 200ms ease; + } \ No newline at end of file diff --git a/src/components/AppNavbar/_data/allMenu.ts b/src/components/AppNavbar/_data/allMenu.ts new file mode 100644 index 0000000..2793aec --- /dev/null +++ b/src/components/AppNavbar/_data/allMenu.ts @@ -0,0 +1,31 @@ +import React from "react"; +import { TbLayoutDashboard, TbUsers } from "react-icons/tb"; + +export interface MenuItem { + label: string, + icon: React.FC, + children?: { + label: string, + link: string, + }[], + color?: string, +} + +const allMenu: MenuItem[] = [ + { + label: "Dashboard", + icon: TbLayoutDashboard, + }, + { + label: "Users", + icon: TbUsers, + color: "green", + children: [ + { label: "Users", link: "#"}, + { label: "Roles", link: "#"}, + { label: "Permissions", link: "#"}, + ] + }, +]; + +export default allMenu; diff --git a/src/app/components/AppNavbar/index.ts b/src/components/AppNavbar/index.ts similarity index 100% rename from src/app/components/AppNavbar/index.ts rename to src/components/AppNavbar/index.ts diff --git a/src/components/DashboardLayout/DashboardLayout.tsx b/src/components/DashboardLayout/DashboardLayout.tsx new file mode 100644 index 0000000..cc35aa2 --- /dev/null +++ b/src/components/DashboardLayout/DashboardLayout.tsx @@ -0,0 +1,35 @@ +"use client"; +import React from "react"; +import { AppShell, AppShellHeader, Burger } from "@mantine/core"; +import AppHeader from "../AppHeader"; +import AppNavbar from "../AppNavbar"; + +import { useDisclosure } from "@mantine/hooks"; + +interface Props { + children: React.ReactNode; +} + +export default function DashboardLayout(props: Props) { + const [openNavbar, { toggle }] = useDisclosure(false); + + return ( + + {/* Header */} + + + {/* Navbar */} + + + {props.children} + + ); +} diff --git a/src/components/DashboardLayout/index.ts b/src/components/DashboardLayout/index.ts new file mode 100644 index 0000000..66b1c0a --- /dev/null +++ b/src/components/DashboardLayout/index.ts @@ -0,0 +1,3 @@ +import DashboardLayout from "./DashboardLayout"; + +export default DashboardLayout;