Reviewed-on: https://git.2fi-solutions.com/wayne.lee/tsms/pulls/1tags/Baseline_30082024_FRONTEND_UAT
| @@ -2446,10 +2446,6 @@ | |||||
| "node": ">= 6" | "node": ">= 6" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/eslint-plugin-import": { | |||||
| "dev": true, | |||||
| "peer": true | |||||
| }, | |||||
| "node_modules/eslint-plugin-prettier": { | "node_modules/eslint-plugin-prettier": { | ||||
| "version": "5.0.1", | "version": "5.0.1", | ||||
| "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", | "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", | 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; | export default components; | ||||
| @@ -79,12 +79,6 @@ const typography: TypographyOptions = { | |||||
| fontSize: "1.125rem", | fontSize: "1.125rem", | ||||
| lineHeight: 1.2, | lineHeight: 1.2, | ||||
| }, | }, | ||||
| fontSize: 0, | |||||
| fontWeightLight: undefined, | |||||
| fontWeightRegular: undefined, | |||||
| fontWeightMedium: undefined, | |||||
| fontWeightBold: undefined, | |||||
| htmlFontSize: 0, | |||||
| }; | }; | ||||
| export default typography; | export default typography; | ||||