diff --git a/eslint.config.js b/eslint.config.js index 74f5075..bcca786 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,7 +18,6 @@ export default tseslint.config( 'react-refresh': reactRefresh, }, rules: { - ...reactHooks.configs.recommended.rules, 'quotes': ['error', 'single'], 'semi': ['error', 'always'], 'indent': ['error', 2], diff --git a/src/App.tsx b/src/App.tsx index ae24122..b460145 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,9 +21,6 @@ const router = createBrowserRouter([ element: , handle: { backButton: false, - sidebar: { - visibile: false - } } as RouteHandleObject }, { diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index d3156c6..327e9dc 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -57,8 +57,8 @@ export const Header = (props: HeaderProps) => { const product: ProductEntity = { id: '0', - title: 'Area Riservata', - productUrl: '#area-riservata', + title: 'Piattaforma Unitaria', + productUrl: '#pu', linkType: 'internal' }; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..c2a23d8 --- /dev/null +++ b/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,144 @@ +import React, { useEffect } from 'react'; +import { + Box, + Divider, + Grid, + IconButton, + List, + Typography, + useTheme, + Tooltip, + useMediaQuery, + type Theme, +} from '@mui/material'; +import { SidebarMenuItem } from './SidebarMenuItem'; +import { useTranslation } from 'react-i18next'; +import MenuIcon from '@mui/icons-material/Menu'; +import CloseIcon from '@mui/icons-material/Close'; +import ViewSidebarIcon from '@mui/icons-material/ViewSidebar'; +import ReceiptLongIcon from '@mui/icons-material/ReceiptLong'; +import AltRouteIcon from '@mui/icons-material/AltRoute'; +import { sidebarStyles } from './sidebar.styles'; +import { PageRoutes } from '../../routes/routes'; +import { ISidebarMenuItem } from '../../models/SidebarMenuItem'; +import useCollapseMenu from '../../hooks/useCollapseMenu'; + +export const Sidebar: React.FC = () => { + + const { t } = useTranslation(); + const theme = useTheme(); + const lg = useMediaQuery((theme: Theme) => theme.breakpoints.up('lg')); + + const { collapsed, changeMenuState, setCollapsed, setOverlay, overlay } = useCollapseMenu(!lg); + + useEffect(() => { + setOverlay(!(lg || collapsed)); + }, [lg, collapsed]); + //This useEffect is needed, otherwise React will complain about the component being re rendered while another re render is in the queue. + + const styles = sidebarStyles(theme, collapsed); + + const RotatedAltRouteIcon = () => { + return ( + + ); + }; + + + const menuItems: Array = [ + { + label: t('menu.homepage'), + icon: ViewSidebarIcon, + route: PageRoutes.HOME, + end: true + }, + { + label: t('menu.debtpositions'), + icon: ReceiptLongIcon, + route: '/debtpositions', + end: true + }, + { + label: t('menu.flows'), + icon: RotatedAltRouteIcon, + route: '/flows', + end: true, + items: [ + { + label: t('menu.subitem'), + route: '/flows/item1', + end: true + }, + ] + } + ]; + + return ( + <> + + + {overlay && ( + + + changeMenuState()} + size="large"> + + + + + )} + + {menuItems.map((item, index) => ( + !lg && setCollapsed(true)} + collapsed={collapsed} + item={item} + key={index} + /> + ))} + + + + + + changeMenuState()} + size="large"> + + {!lg && ( + + {t('menu.menu')} + + )} + + + + + + + {overlay && } + + ); +}; diff --git a/src/components/Sidebar/SidebarMenuItem.tsx b/src/components/Sidebar/SidebarMenuItem.tsx new file mode 100644 index 0000000..293705a --- /dev/null +++ b/src/components/Sidebar/SidebarMenuItem.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText, useTheme } from '@mui/material'; +import { NavLink } from 'react-router-dom'; +import { SvgIconComponent } from '@mui/icons-material'; +import { alpha } from '@mui/material'; +import { ISidebarMenuItem } from '../../models/SidebarMenuItem'; +import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded'; +import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; + +type Props = { + collapsed: boolean; + item: ISidebarMenuItem; + onClick: React.MouseEventHandler | undefined; +}; + +function renderIcon(Icon: SvgIconComponent | (() => JSX.Element)) { + return ; +} + +export const SidebarMenuItem = ({ collapsed, item, onClick }: Props) => { + const theme = useTheme(); + const [selectedTarget, setSelectedTarget] = React.useState(''); + const [open, setOpen] = React.useState(false); + const handleCollapseClick = () => { + setOpen(!open); + }; + const handleListItemClick = (target: string) => { + setSelectedTarget(target); + }; + + return ( + + + {item.icon && } + {!collapsed && ( + + )} + {item.items && + (open ? : )} + + + {item.items && + + + {item.items.map((subitem, subindex) => ( + handleListItemClick(`subitem-${subindex}`)}> + + + ))} + + + } + + ); +}; diff --git a/src/components/Sidebar/sidebar.styles.ts b/src/components/Sidebar/sidebar.styles.ts new file mode 100644 index 0000000..e87f080 --- /dev/null +++ b/src/components/Sidebar/sidebar.styles.ts @@ -0,0 +1,61 @@ +import { SxProps, Theme } from '@mui/material'; + +export const sidebarStyles = (theme: Theme, collapsed: boolean): Record => ({ + container: { + zIndex: collapsed ? 1 : 10, + position: collapsed ? 'relative' : 'fixed', + width: '100%', + top: 0, + height: '100vh', + transition: 'width 0.3s ease, height 0.3s ease', // Add transition for smooth resizing + [theme.breakpoints.between('sm', 'lg')]: { width: collapsed ? '100%' : 'fit-content' }, + [theme.breakpoints.up('lg')]: { width: 'fit-content', position: 'sticky' }, + [theme.breakpoints.down('lg')]: { height: collapsed ? 'fit-content' : '100%' } + }, + nav: { + minHeight: collapsed ? '1vh' : '50vh', + height: '100%', + width: '100%', + bgcolor: 'background.paper', + transition: 'width 0.3s ease', // Add transition for smooth width change + [theme.breakpoints.up('sm')]: { width: collapsed ? '100%' : '300px' }, + [theme.breakpoints.up('lg')]: { width: collapsed ? '88px' : '300px', minHeight: '50vh' } + }, + overlay: { + bgcolor: 'rgba(23, 50, 77, 0.7)', + zIndex: 1, + position: 'fixed', + top: 0, + left: 0, + height: '100%', + width: '100%' + }, + collapseIcon: { + textAlign: 'right', + pt: 1, + pr: 2 + }, + list: { + [theme.breakpoints.down('lg')]: { + display: collapsed ? 'none' : 'inline-block' + } + }, + hamburgerBox: { + marginTop: 'auto', + position: 'sticky', + bottom: '0', + transition: 'opacity 0.3s ease', // Add transition for smooth visibility change + [theme.breakpoints.down('lg')]: { + marginTop: collapsed ? 0 : 'auto', + opacity: collapsed ? 1 : 0, + visibility: collapsed ? 'visible' : 'hidden' + } + }, + hamburgerIcon: { + p: 2 + }, + hamburgerTypography: { + fontWeight: 600, + pl: 1 + } +}); diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 3c3ebaf..5873487 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -6,6 +6,8 @@ import { NavigateNext } from '@mui/icons-material'; import Breadcrumbs from '../Breadcrumbs/Breadcrumbs'; import { RouteHandleObject } from '../../models/Breadcrumbs'; import { Header } from '../Header'; +import { Sidebar } from '../Sidebar/Sidebar'; +import utils from '../../utils'; const defaultRouteHandle: RouteHandleObject = { sidebar: { visible: true }, @@ -16,11 +18,17 @@ const defaultRouteHandle: RouteHandleObject = { export function Layout() { const matches = useMatches(); - const { crumbs, backButton, backButtonText, backButtonFunction } = { + const overlay = utils.sidemenu.status.overlay.value; + + document.body.style.overflow = overlay ? 'hidden' : 'auto'; + + const { crumbs, sidebar, backButton, backButtonText, backButtonFunction } = { ...defaultRouteHandle, ...(matches.find((match) => Boolean(match.handle))?.handle || {}) } as RouteHandleObject; + const sidePadding = sidebar.visible ? 3 : { xs: 3, md: 12, lg: 27, xl: 34 }; + return ( <> - SIDEBAR - + {sidebar?.visible ? : null} + {backButton && } {crumbs && ( } /> diff --git a/src/hooks/useCollapseMenu.tsx b/src/hooks/useCollapseMenu.tsx new file mode 100644 index 0000000..8de57a4 --- /dev/null +++ b/src/hooks/useCollapseMenu.tsx @@ -0,0 +1,40 @@ +import useMediaQuery from '@mui/material/useMediaQuery'; +import { useEffect, useState } from 'react'; +import utils from '../utils'; + +function useCollapseMenu(initialCollapsedState: boolean) { + useEffect(() => { + utils.sidemenu.setCollapsed(initialCollapsedState); + utils.sidemenu.setOverlay(false); + }, []); + const theme = utils.style.theme; + + const isBelowLg = useMediaQuery(theme.breakpoints.down('lg')); + const [wasBelowLg, setWasBelowLg] = useState(isBelowLg); + + const collapsed = utils.sidemenu.status.isMenuCollapsed.value; + const overlay = utils.sidemenu.status.overlay.value; + + const changeMenuState = () => utils.sidemenu.setCollapsed(!collapsed); + const setCollapsed = (value: boolean) => utils.sidemenu.setCollapsed(value); + const setOverlay = (overlayActive: boolean) => utils.sidemenu.setOverlay(overlayActive); + + useEffect(() => { + if (isBelowLg && !wasBelowLg) { + setCollapsed(true); + } else if (!isBelowLg && wasBelowLg) { + setCollapsed(false); + } + setWasBelowLg(isBelowLg); + }, [isBelowLg]); + + return { + collapsed, + overlay, + setOverlay, + setCollapsed, + changeMenuState + }; +} + +export default useCollapseMenu; diff --git a/src/models/SidebarMenuItem.ts b/src/models/SidebarMenuItem.ts new file mode 100644 index 0000000..91a8b5a --- /dev/null +++ b/src/models/SidebarMenuItem.ts @@ -0,0 +1,11 @@ +import { SvgIconComponent } from '@mui/icons-material'; + +export interface ISidebarMenuItem { + label: string; + icon?: SvgIconComponent | (() => JSX.Element); + route: string; + /* The end prop changes the matching logic for the active and pending states to only match to the "end" of the NavLink's to path. + If the URL is longer than to, it will no longer be considered active. */ + end?: boolean; + items?: [ISidebarMenuItem] +} diff --git a/src/translations/it/translations.json b/src/translations/it/translations.json index cc3668f..54f46bc 100644 --- a/src/translations/it/translations.json +++ b/src/translations/it/translations.json @@ -1,8 +1,15 @@ { "app": { - "routes": { - "back": "Indietro" + "routes": { + "back": "Indietro" + } + }, + "menu": { + "menu": "Menu", + "homepage": "Panoramica", + "debtpositions": "Dovuti", + "flows": "Flussi", + "subitem": "Item 1" } } -} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 70012e1..1496f00 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,9 @@ import config from './config'; import style from './style'; +import sidemenu from './sidemenu'; export default { config, + sidemenu, style }; diff --git a/src/utils/sidemenu.tsx b/src/utils/sidemenu.tsx new file mode 100644 index 0000000..3f6f027 --- /dev/null +++ b/src/utils/sidemenu.tsx @@ -0,0 +1,25 @@ +import { signal } from '@preact/signals-react'; + +const changeMenuState = () => { + isMenuCollapsed.value = !isMenuCollapsed.value; +}; +const setCollapsed = (isCollapsed: boolean) => { + isMenuCollapsed.value = isCollapsed; +}; + +const setOverlay = (overlayActive: boolean) => { + overlay.value = overlayActive; +}; + +const isMenuCollapsed = signal(false); +const overlay = signal(false); + +export default { + changeMenuState, + setCollapsed, + setOverlay, + status: { + isMenuCollapsed, + overlay + } +};