| @@ -2446,10 +2446,6 @@ | |||
| "node": ">= 6" | |||
| } | |||
| }, | |||
| "node_modules/eslint-plugin-import": { | |||
| "dev": true, | |||
| "peer": true | |||
| }, | |||
| "node_modules/eslint-plugin-prettier": { | |||
| "version": "5.0.1", | |||
| "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", | |||
| @@ -0,0 +1,32 @@ | |||
| import type { Metadata } from "next"; | |||
| import AppBar from "@/components/AppBar"; | |||
| import { getServerSession } from "next-auth"; | |||
| import { authOptions } from "@/config/authConfig"; | |||
| import { redirect } from "next/navigation"; | |||
| export const metadata: Metadata = { | |||
| title: "Dashboard", | |||
| }; | |||
| export default async function DashboardLayout({ | |||
| children, | |||
| }: { | |||
| children: React.ReactNode; | |||
| }) { | |||
| const session = await getServerSession(authOptions); | |||
| console.log(session); | |||
| if (!session?.user) { | |||
| redirect("/login"); | |||
| } | |||
| return ( | |||
| <> | |||
| <AppBar | |||
| profileName={session.user.name!} | |||
| avatarImageSrc={session.user.image || undefined} | |||
| /> | |||
| <main>{children}</main> | |||
| </> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| import MUIAppBar from "@mui/material/AppBar"; | |||
| import Toolbar from "@mui/material/Toolbar"; | |||
| import React from "react"; | |||
| import Profile from "./Profile"; | |||
| import Box from "@mui/material/Box"; | |||
| import NavigationToggle from "./NavigationToggle"; | |||
| import { I18nProvider } from "@/i18n"; | |||
| export interface AppBarProps { | |||
| avatarImageSrc?: string; | |||
| profileName: string; | |||
| } | |||
| const AppBar: React.FC<AppBarProps> = ({ avatarImageSrc, profileName }) => { | |||
| return ( | |||
| <MUIAppBar position="fixed"> | |||
| <Toolbar> | |||
| <I18nProvider namespaces={["common"]}> | |||
| <NavigationToggle /> | |||
| <Box | |||
| sx={{ flexGrow: 1, display: "flex", justifyContent: "flex-end" }} | |||
| > | |||
| <Profile | |||
| avatarImageSrc={avatarImageSrc} | |||
| profileName={profileName} | |||
| /> | |||
| </Box> | |||
| </I18nProvider> | |||
| </Toolbar> | |||
| </MUIAppBar> | |||
| ); | |||
| }; | |||
| export default AppBar; | |||
| @@ -0,0 +1,47 @@ | |||
| "use client"; | |||
| import IconButton from "@mui/material/IconButton"; | |||
| import MenuIcon from "@mui/icons-material/Menu"; | |||
| import NavigationContent from "../NavigationContent"; | |||
| import React from "react"; | |||
| import Drawer from "@mui/material/Drawer"; | |||
| const NavigationToggle: React.FC = () => { | |||
| const [isOpened, setIsOpened] = React.useState(false); | |||
| const openNavigation = () => { | |||
| setIsOpened(true); | |||
| }; | |||
| const closeNavigation = () => { | |||
| setIsOpened(false); | |||
| }; | |||
| return ( | |||
| <> | |||
| <Drawer variant="permanent" sx={{ display: { xs: "none", lg: "block" } }}> | |||
| <NavigationContent /> | |||
| </Drawer> | |||
| <Drawer | |||
| sx={{ display: { lg: "none" } }} | |||
| open={isOpened} | |||
| onClose={closeNavigation} | |||
| ModalProps={{ | |||
| keepMounted: true, | |||
| }} | |||
| > | |||
| <NavigationContent /> | |||
| </Drawer> | |||
| <IconButton | |||
| sx={{ display: { lg: "none" } }} | |||
| onClick={openNavigation} | |||
| edge="start" | |||
| aria-label="menu" | |||
| color="inherit" | |||
| > | |||
| <MenuIcon fontSize="inherit" /> | |||
| </IconButton> | |||
| </> | |||
| ); | |||
| }; | |||
| export default NavigationToggle; | |||
| @@ -0,0 +1,61 @@ | |||
| "use client"; | |||
| import IconButton from "@mui/material/IconButton"; | |||
| import Menu from "@mui/material/Menu"; | |||
| import MenuItem from "@mui/material/MenuItem"; | |||
| import Avatar from "@mui/material/Avatar"; | |||
| import React from "react"; | |||
| import { AppBarProps } from "./AppBar"; | |||
| import Divider from "@mui/material/Divider"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { signOut } from "next-auth/react"; | |||
| type Props = Pick<AppBarProps, "avatarImageSrc" | "profileName">; | |||
| const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => { | |||
| const [profileMenuAnchorEl, setProfileMenuAnchorEl] = | |||
| React.useState<HTMLButtonElement>(); | |||
| const openProfileMenu: React.MouseEventHandler<HTMLButtonElement> = ( | |||
| event, | |||
| ) => { | |||
| setProfileMenuAnchorEl(event.currentTarget); | |||
| }; | |||
| const closeProfileMenu = () => { | |||
| setProfileMenuAnchorEl(undefined); | |||
| }; | |||
| const { t } = useTranslation("login"); | |||
| return ( | |||
| <> | |||
| <IconButton aria-label="profile" onClick={openProfileMenu}> | |||
| <Avatar src={avatarImageSrc} /> | |||
| </IconButton> | |||
| <Menu | |||
| id="profile-menu" | |||
| anchorEl={profileMenuAnchorEl} | |||
| anchorOrigin={{ | |||
| vertical: "bottom", | |||
| horizontal: "right", | |||
| }} | |||
| keepMounted | |||
| transformOrigin={{ | |||
| vertical: "top", | |||
| horizontal: "right", | |||
| }} | |||
| open={Boolean(profileMenuAnchorEl)} | |||
| onClose={closeProfileMenu} | |||
| MenuListProps={{ dense: true, disablePadding: true }} | |||
| > | |||
| <Typography sx={{ mx: "1.5rem", my: "0.5rem" }} fontWeight="bold"> | |||
| {profileName} | |||
| </Typography> | |||
| <Divider /> | |||
| <MenuItem onClick={() => signOut()}>{t("Sign out")}</MenuItem> | |||
| </Menu> | |||
| </> | |||
| ); | |||
| }; | |||
| export default Profile; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./AppBar"; | |||
| @@ -0,0 +1,66 @@ | |||
| import Divider from "@mui/material/Divider"; | |||
| import Box from "@mui/material/Box"; | |||
| import React from "react"; | |||
| import List from "@mui/material/List"; | |||
| import ListItemButton from "@mui/material/ListItemButton"; | |||
| import ListItemText from "@mui/material/ListItemText"; | |||
| import ListItemIcon from "@mui/material/ListItemIcon"; | |||
| import WorkHistory from "@mui/icons-material/WorkHistory"; | |||
| import Dashboard from "@mui/icons-material/Dashboard"; | |||
| import RequestQuote from "@mui/icons-material/RequestQuote"; | |||
| import Task from "@mui/icons-material/Task"; | |||
| import Assignment from "@mui/icons-material/Assignment"; | |||
| import Settings from "@mui/icons-material/Settings"; | |||
| import Analytics from "@mui/icons-material/Analytics"; | |||
| import Payments from "@mui/icons-material/Payments"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { usePathname } from "next/navigation"; | |||
| import Link from "next/link"; | |||
| interface NavigationItem { | |||
| icon: React.ReactNode; | |||
| label: string; | |||
| path: string; | |||
| } | |||
| const navigationItems: NavigationItem[] = [ | |||
| { icon: <WorkHistory />, label: "User Workspace", path: "/workspace" }, | |||
| { icon: <Dashboard />, label: "Dashboard", path: "/dashboard" }, | |||
| { icon: <RequestQuote />, label: "Expense Claim", path: "/claim" }, | |||
| { icon: <Assignment />, label: "Project Management", path: "/projects" }, | |||
| { icon: <Task />, label: "Task Template", path: "/tasks" }, | |||
| { icon: <Payments />, label: "Invoice", path: "/invoice" }, | |||
| { icon: <Analytics />, label: "Analysis Report", path: "/analytics" }, | |||
| { icon: <Settings />, label: "Setting", path: "/settings" }, | |||
| ]; | |||
| const NavigationContent: React.FC = () => { | |||
| const { t } = useTranslation("common"); | |||
| const pathname = usePathname(); | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ p: "1.5rem" }}> | |||
| {/* Replace this with company logo and/or name */} | |||
| <Typography variant="h4">TSMS</Typography> | |||
| </Box> | |||
| <Divider /> | |||
| <List component="nav"> | |||
| {navigationItems.map(({ icon, label, path }, index) => { | |||
| return ( | |||
| <ListItemButton | |||
| key={`${label}-${index}`} | |||
| selected={pathname.includes(path)} | |||
| > | |||
| <ListItemIcon>{icon}</ListItemIcon> | |||
| <ListItemText primary={<Link href={path}>{t(label)}</Link>} /> | |||
| </ListItemButton> | |||
| ); | |||
| })} | |||
| </List> | |||
| </Box> | |||
| ); | |||
| }; | |||
| export default NavigationContent; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./NavigationContent"; | |||
| @@ -283,6 +283,51 @@ const components: ThemeOptions["components"] = { | |||
| variant: "filled", | |||
| }, | |||
| }, | |||
| MuiMenuItem: { | |||
| styleOverrides: { | |||
| root: { | |||
| margin: "0.5rem", | |||
| borderRadius: 8, | |||
| }, | |||
| }, | |||
| }, | |||
| MuiList: { | |||
| styleOverrides: { | |||
| padding: { | |||
| paddingBlock: "1rem", | |||
| paddingInline: "1rem", | |||
| }, | |||
| }, | |||
| }, | |||
| MuiListItemButton: { | |||
| styleOverrides: { | |||
| root: { | |||
| borderRadius: 8, | |||
| marginBlockEnd: "0.5rem", | |||
| a: { | |||
| textDecoration: "none", | |||
| color: "inherit", | |||
| } | |||
| }, | |||
| }, | |||
| }, | |||
| MuiListItemText: { | |||
| styleOverrides: { | |||
| root: { | |||
| marginInlineEnd: "2rem", | |||
| }, | |||
| primary: { | |||
| fontWeight: 500, | |||
| }, | |||
| }, | |||
| }, | |||
| MuiListItemIcon: { | |||
| styleOverrides: { | |||
| root: { | |||
| color: "inherit", | |||
| }, | |||
| }, | |||
| }, | |||
| }; | |||
| export default components; | |||
| @@ -79,12 +79,6 @@ const typography: TypographyOptions = { | |||
| fontSize: "1.125rem", | |||
| lineHeight: 1.2, | |||
| }, | |||
| fontSize: 0, | |||
| fontWeightLight: undefined, | |||
| fontWeightRegular: undefined, | |||
| fontWeightMedium: undefined, | |||
| fontWeightBold: undefined, | |||
| htmlFontSize: 0, | |||
| }; | |||
| export default typography; | |||