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; |