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 && {renderIcon(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
+ }
+};