diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx new file mode 100644 index 0000000000..f87c1a751e --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx @@ -0,0 +1,79 @@ +import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined'; +import type SvgIcon from '@mui/material/SvgIcon/SvgIcon'; +import ApplicationsIcon from '@mui/icons-material/AppsOutlined'; +import ContextFieldsIcon from '@mui/icons-material/AccountTreeOutlined'; +import FlagTypesIcon from '@mui/icons-material/OutlinedFlag'; +import IntegrationsIcon from '@mui/icons-material/IntegrationInstructionsOutlined'; +import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined'; +import CustomStrategiesIcon from '@mui/icons-material/ExtensionOutlined'; +import TagTypesIcon from '@mui/icons-material/LabelImportantOutlined'; +import EnvironmentsIcon from '@mui/icons-material/CloudOutlined'; +import UsersIcon from '@mui/icons-material/GroupOutlined'; +import ServiceAccountIcon from '@mui/icons-material/SmartToyOutlined'; +import GroupsIcon from '@mui/icons-material/GroupsOutlined'; +import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined'; +import ApiAccessIcon from '@mui/icons-material/KeyOutlined'; +import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined'; +import NetworkIcon from '@mui/icons-material/HubOutlined'; +import MaintenanceIcon from '@mui/icons-material/BuildOutlined'; +import BannersIcon from '@mui/icons-material/PhotoOutlined'; +import InstanceStatsIcon from '@mui/icons-material/QueryStatsOutlined'; +import LicenseIcon from '@mui/icons-material/ReceiptLongOutlined'; +import InstancePrivacyIcon from '@mui/icons-material/ShieldOutlined'; +import LoginHistoryIcon from '@mui/icons-material/HistoryOutlined'; +import CorsIcon from '@mui/icons-material/StorageOutlined'; +import BillingIcon from '@mui/icons-material/CreditCardOutlined'; +import EventLogIcon from '@mui/icons-material/EventNoteOutlined'; +import GitHubIcon from '@mui/icons-material/GitHub'; +import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; +import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; +import type { FC } from 'react'; +import { styled } from '@mui/material'; + +const icons: Record = { + '/applications': ApplicationsIcon, + '/context': ContextFieldsIcon, + '/feature-toggle-type': FlagTypesIcon, + '/integrations': IntegrationsIcon, + '/segments': SegmentsIcon, + '/strategies': CustomStrategiesIcon, + '/tag-types': TagTypesIcon, + '/environments': EnvironmentsIcon, + '/admin/users': UsersIcon, + '/admin/service-accounts': ServiceAccountIcon, + '/admin/groups': GroupsIcon, + '/admin/roles': RoleIcon, + '/admin/api': ApiAccessIcon, + '/admin/auth': SingleSignOnIcon, + '/admin/network': NetworkIcon, + '/admin/maintenance': MaintenanceIcon, + '/admin/banners': BannersIcon, + '/admin/instance': InstanceStatsIcon, + '/admin/license': LicenseIcon, + '/admin/instance-privacy': InstancePrivacyIcon, + '/admin/logins': LoginHistoryIcon, + '/admin/cors': CorsIcon, + '/admin/billing': BillingIcon, + '/history': EventLogIcon, + GitHub: GitHubIcon, + Documentation: LibraryBooksIcon, +}; + +const findIcon = (key: string) => { + return icons[key] || EmptyIcon; +}; + +export const IconRenderer: FC<{ path: string }> = ({ path }) => { + const IconComponent = findIcon(path); // Fallback to 'default' if the type is not found + + return ; +}; + +export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ + fill: theme.palette.neutral.main, + stroke: theme.palette.neutral.main, + // same as built-in icons + width: theme.spacing(3), + height: theme.spacing(3), + fontSize: theme.spacing(3), +})); diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx new file mode 100644 index 0000000000..c11ac7cb73 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/ListItems.tsx @@ -0,0 +1,108 @@ +import type { FC, ReactNode } from 'react'; +import { + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Tooltip, +} from '@mui/material'; +import { Link } from 'react-router-dom'; +import { basePath } from 'utils/formatPath'; +import SignOutIcon from '@mui/icons-material/ExitToApp'; +import type { Theme } from '@mui/material/styles/createTheme'; + +const listItemButtonStyle = (theme: Theme) => ({ + borderRadius: theme.spacing(0.5), + borderLeft: `${theme.spacing(0.5)} solid transparent`, + '&:hover': { + borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`, + }, +}); + +export const FullListItem: FC<{ + href: string; + text: string; + badge?: ReactNode; + onClick?: () => void; +}> = ({ href, text, badge, onClick, children }) => { + return ( + + + ({ minWidth: theme.spacing(4) })}> + {children} + + + {badge} + + + ); +}; + +export const ExternalFullListItem: FC<{ href: string; text: string }> = ({ + href, + text, + children, +}) => { + return ( + + + ({ minWidth: theme.spacing(4) })}> + {children} + + + + + ); +}; +export const SignOutItem = () => { + return ( +
+ + + ({ minWidth: theme.spacing(4) })} + > + + + + + +
+ ); +}; + +export const MiniListItem: FC<{ href: string; text: string }> = ({ + href, + text, + children, +}) => { + return ( + + + + ({ minWidth: theme.spacing(4) })} + > + {children} + + + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx new file mode 100644 index 0000000000..a4f061d407 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationList.tsx @@ -0,0 +1,179 @@ +import { type FC, type ReactNode, useCallback } from 'react'; +import type { INavigationMenuItem } from 'interfaces/route'; +import type { NavigationMode } from './NavigationMode'; +import { + ExternalFullListItem, + FullListItem, + MiniListItem, + SignOutItem, +} from './ListItems'; +import { List, styled, Tooltip, Typography } from '@mui/material'; +import { IconRenderer, StyledProjectIcon } from './IconRenderer'; +import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import SearchIcon from '@mui/icons-material/Search'; +import PlaygroundIcon from '@mui/icons-material/AutoFixNormal'; +import InsightsIcon from '@mui/icons-material/Insights'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + +const StyledBadgeContainer = styled('div')(({ theme }) => ({ + paddingLeft: theme.spacing(2), + display: 'flex', +})); + +const EnterprisePlanBadge = () => ( + + + + + +); + +const useShowBadge = () => { + const { isPro } = useUiConfig(); + + const showBadge = useCallback( + (mode?: INavigationMenuItem['menu']['mode']) => { + return !!( + isPro() && + !mode?.includes('pro') && + mode?.includes('enterprise') + ); + }, + [isPro], + ); + return showBadge; +}; + +export const ConfigureNavigationList: FC<{ + routes: INavigationMenuItem[]; + mode: NavigationMode; + onClick?: () => void; +}> = ({ routes, mode, onClick }) => { + const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; + + return ( + + {routes.map((route) => ( + + + + ))} + + ); +}; +export const AdminNavigationList: FC<{ + routes: INavigationMenuItem[]; + mode: NavigationMode; + badge?: ReactNode; + onClick?: () => void; +}> = ({ routes, mode, onClick, badge }) => { + const showBadge = useShowBadge(); + const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; + + return ( + + {routes.map((route) => ( + + ) : null + } + > + + + ))} + + ); +}; + +export const OtherLinksList = () => { + const { uiConfig } = useUiConfig(); + + return ( + + {uiConfig.links.map((link) => ( + + + + ))} + + + ); +}; + +export const PrimaryNavigationList: FC<{ + mode: NavigationMode; + onClick?: () => void; +}> = ({ mode, onClick }) => { + const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; + + return ( + + + + + + + + + + + + + + + ); +}; + +const AccordionHeader: FC = ({ children }) => { + return ( + } + aria-controls='configure-content' + id='configure-header' + > + + {children} + + + ); +}; + +export const SecondaryNavigation: FC<{ + expanded: boolean; + onChange: (expanded: boolean) => void; + mode: NavigationMode; + routes: INavigationMenuItem[]; +}> = ({ mode, expanded, onChange, routes, children }) => { + return ( + { + onChange(expand); + }} + > + {mode === 'full' && {children}} + + + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationMode.ts b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationMode.ts new file mode 100644 index 0000000000..d6ce6b2614 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationMode.ts @@ -0,0 +1 @@ +export type NavigationMode = 'mini' | 'full'; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx index 8dea9c3d39..de78754d96 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx @@ -1,427 +1,16 @@ +import { Box, styled } from '@mui/material'; +import type { FC } from 'react'; +import { useNavigationMode } from './useNavigationMode'; +import { ShowHide } from './ShowHide'; +import { useRoutes } from './useRoutes'; +import { useExpanded } from './useExpanded'; import { - Box, - IconButton, - List, - ListItem, - ListItemButton, - ListItemIcon, - ListItemText, - styled, - Tooltip, - Typography, -} from '@mui/material'; -import { Link } from 'react-router-dom'; -import SearchIcon from '@mui/icons-material/Search'; -import PlaygroundIcon from '@mui/icons-material/AutoFixNormal'; -import InsightsIcon from '@mui/icons-material/Insights'; -import Accordion from '@mui/material/Accordion'; -import AccordionSummary from '@mui/material/AccordionSummary'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import AccordionDetails from '@mui/material/AccordionDetails'; -import IntegrationsIcon from '@mui/icons-material/IntegrationInstructionsOutlined'; -import EnvironmentsIcon from '@mui/icons-material/CloudOutlined'; -import ContextFieldsIcon from '@mui/icons-material/AccountTreeOutlined'; -import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined'; -import TagTypesIcon from '@mui/icons-material/LabelImportantOutlined'; -import ApplicationsIcon from '@mui/icons-material/AppsOutlined'; -import CustomStrategiesIcon from '@mui/icons-material/ExtensionOutlined'; -import UsersIcon from '@mui/icons-material/GroupOutlined'; -import ServiceAccountIcon from '@mui/icons-material/SmartToyOutlined'; -import GroupsIcon from '@mui/icons-material/GroupsOutlined'; -import RoleIcon from '@mui/icons-material/AdminPanelSettingsOutlined'; -import ApiAccessIcon from '@mui/icons-material/KeyOutlined'; -import SingleSignOnIcon from '@mui/icons-material/AssignmentOutlined'; -import NetworkIcon from '@mui/icons-material/HubOutlined'; -import MaintenanceIcon from '@mui/icons-material/BuildOutlined'; -import BannersIcon from '@mui/icons-material/PhotoOutlined'; -import InstanceStatsIcon from '@mui/icons-material/QueryStatsOutlined'; -import LicenseIcon from '@mui/icons-material/ReceiptLongOutlined'; -import InstancePrivacyIcon from '@mui/icons-material/ShieldOutlined'; -import LoginHistoryIcon from '@mui/icons-material/HistoryOutlined'; -import EventLogIcon from '@mui/icons-material/EventNoteOutlined'; -import FlagTypesIcon from '@mui/icons-material/OutlinedFlag'; -import EmptyIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined'; -import CorsIcon from '@mui/icons-material/StorageOutlined'; -import BillingIcon from '@mui/icons-material/CreditCardOutlined'; -import SignOutIcon from '@mui/icons-material/ExitToApp'; -import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; -import { type FC, type ReactNode, useCallback, useEffect } from 'react'; -import { getCondensedRoutes, getRoutes } from '../../../menu/routes'; -import { useAdminRoutes } from '../../../admin/useAdminRoutes'; -import { filterByConfig, mapRouteLink } from 'component/common/util'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import type SvgIcon from '@mui/material/SvgIcon/SvgIcon'; -import { EnterpriseBadge } from 'component/common/EnterpriseBadge/EnterpriseBadge'; -import type { INavigationMenuItem } from 'interfaces/route'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; -import { basePath } from 'utils/formatPath'; -import { useLocalStorageState } from 'hooks/useLocalStorageState'; -import type { Theme } from '@mui/material/styles/createTheme'; -import { unique } from 'utils/unique'; - -export const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ - fill: theme.palette.neutral.main, - stroke: theme.palette.neutral.main, - // same as built-in icons - width: theme.spacing(3), - height: theme.spacing(3), - fontSize: theme.spacing(3), -})); - -const StyledBadgeContainer = styled('div')(({ theme }) => ({ - paddingLeft: theme.spacing(2), - display: 'flex', -})); - -const EnterprisePlanBadge = () => ( - - - - - -); - -const listItemButtonStyle = (theme: Theme) => ({ - borderRadius: theme.spacing(0.5), - borderLeft: `${theme.spacing(0.5)} solid transparent`, - '&:hover': { - borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`, - }, -}); - -const FullListItem: FC<{ - href: string; - text: string; - badge?: ReactNode; - onClick?: () => void; -}> = ({ href, text, badge, onClick, children }) => { - return ( - - - ({ minWidth: theme.spacing(4) })}> - {children} - - - {badge} - - - ); -}; - -const ExternalFullListItem: FC<{ href: string; text: string }> = ({ - href, - text, - children, -}) => { - return ( - - - ({ minWidth: theme.spacing(4) })}> - {children} - - - - - ); -}; - -const SignOutItem = () => { - return ( -
- - - ({ minWidth: theme.spacing(4) })} - > - - - - - -
- ); -}; - -const MiniListItem: FC<{ href: string; text: string }> = ({ - href, - text, - children, -}) => { - return ( - - - - ({ minWidth: theme.spacing(4) })} - > - {children} - - - - - ); -}; - -export const StyledBox = styled(Box)(({ theme }) => ({ - backgroundColor: theme.palette.background.paper, - padding: theme.spacing(2, 1, 6, 1), -})); - -const icons: Record = { - '/applications': ApplicationsIcon, - '/context': ContextFieldsIcon, - '/feature-toggle-type': FlagTypesIcon, - '/integrations': IntegrationsIcon, - '/segments': SegmentsIcon, - '/strategies': CustomStrategiesIcon, - '/tag-types': TagTypesIcon, - '/environments': EnvironmentsIcon, - '/admin/users': UsersIcon, - '/admin/service-accounts': ServiceAccountIcon, - '/admin/groups': GroupsIcon, - '/admin/roles': RoleIcon, - '/admin/api': ApiAccessIcon, - '/admin/auth': SingleSignOnIcon, - '/admin/network': NetworkIcon, - '/admin/maintenance': MaintenanceIcon, - '/admin/banners': BannersIcon, - '/admin/instance': InstanceStatsIcon, - '/admin/license': LicenseIcon, - '/admin/instance-privacy': InstancePrivacyIcon, - '/admin/logins': LoginHistoryIcon, - '/admin/cors': CorsIcon, - '/admin/billing': BillingIcon, - '/history': EventLogIcon, - GitHub: GitHubIcon, - Documentation: LibraryBooksIcon, -}; - -const findIconByPath = (path: string) => { - return icons[path] || EmptyIcon; -}; - -const IconRenderer: FC<{ path: string }> = ({ path }) => { - const IconComponent = findIconByPath(path); // Fallback to 'default' if the type is not found - - return ; -}; - -const ShowHideWrapper = styled(Box, { - shouldForwardProp: (prop) => prop !== 'mode', -})<{ mode: NavigationMode }>(({ theme, mode }) => ({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - margin: theme.spacing(2, 1, 0, mode === 'mini' ? 1.5 : 2), - cursor: 'pointer', -})); - -const ShowHide: FC<{ mode: NavigationMode; onChange: () => void }> = ({ - mode, - onChange, -}) => { - return ( - - {mode === 'full' && ( - ({ - color: theme.palette.neutral.main, - fontSize: 'small', - })} - > - Hide (⌘ + B) - - )} - - {mode === 'full' ? ( - - ) : ( - - - - )} - - - ); -}; - -const useRoutes = () => { - const { uiConfig } = useUiConfig(); - const routes = getRoutes(); - const adminRoutes = useAdminRoutes(); - - const filteredMainRoutes = { - mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes) - .filter(filterByConfig(uiConfig)) - .map(mapRouteLink), - mobileRoutes: getCondensedRoutes(routes.mobileRoutes) - .filter(filterByConfig(uiConfig)) - .map(mapRouteLink), - adminRoutes, - }; - - return { routes: filteredMainRoutes }; -}; - -const useShowBadge = () => { - const { isPro } = useUiConfig(); - - const showBadge = useCallback( - (mode?: INavigationMenuItem['menu']['mode']) => { - return !!( - isPro() && - !mode?.includes('pro') && - mode?.includes('enterprise') - ); - }, - [isPro], - ); - return showBadge; -}; - -type NavigationMode = 'mini' | 'full'; - -const useNavigationMode = () => { - const [mode, setMode] = useLocalStorageState( - 'navigation-mode:v1', - 'full', - ); - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'b' && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - setMode(mode === 'mini' ? 'full' : 'mini'); - } - }; - - document.addEventListener('keydown', handleKeyDown); - - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; - }, [mode]); - - return [mode, setMode] as const; -}; - -const MainNavigationList: FC<{ mode: NavigationMode; onClick?: () => void }> = - ({ mode, onClick }) => { - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; - - return ( - - - - - - - - - - - - - - - ); - }; - -const ConfigureNavigationList: FC<{ - routes: INavigationMenuItem[]; - mode: NavigationMode; - onClick?: () => void; -}> = ({ routes, mode, onClick }) => { - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; - - return ( - - {routes.map((route) => ( - - - - ))} - - ); -}; - -const AdminNavigationList: FC<{ - routes: INavigationMenuItem[]; - mode: NavigationMode; - badge?: ReactNode; - onClick?: () => void; -}> = ({ routes, mode, onClick, badge }) => { - const showBadge = useShowBadge(); - const DynamicListItem = mode === 'mini' ? MiniListItem : FullListItem; - - return ( - - {routes.map((route) => ( - - ) : null - } - > - - - ))} - - ); -}; - -const OtherLinksList = () => { - const { uiConfig } = useUiConfig(); - - return ( - - {uiConfig.links.map((link) => ( - - - - ))} - - - ); -}; + AdminNavigationList, + ConfigureNavigationList, + OtherLinksList, + PrimaryNavigationList, + SecondaryNavigation, +} from './NavigationList'; export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({ onClick, @@ -430,7 +19,7 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({ return ( <> - + void }> = ({ ); }; -const useExpanded = () => { - const [expanded, setExpanded] = useLocalStorageState>( - 'navigation-expanded:v1', - [], - ); - - const changeExpanded = (key: T, expand: boolean) => { - if (expand) { - setExpanded(unique([...expanded, key])); - } else { - setExpanded(expanded.filter((name) => name !== key)); - } - }; - - return [expanded, changeExpanded] as const; -}; +export const StyledBox = styled(Box)(({ theme }) => ({ + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(2, 1, 6, 1), +})); export const NavigationSidebar = () => { const { routes } = useRoutes(); @@ -471,64 +48,27 @@ export const NavigationSidebar = () => { return ( - - + { + onChange={(expand) => { changeExpanded('configure', expand); }} + mode={mode} + routes={routes.mainNavRoutes} > - {mode === 'full' && ( - } - aria-controls='configure-content' - id='configure-header' - > - - Configure - - - )} - - - - - + { + onChange={(expand) => { changeExpanded('admin', expand); }} + mode={mode} + routes={routes.adminRoutes} > - {mode === 'full' && ( - } - aria-controls='admin-content' - id='admin-header' - > - - Admin - - - )} - - - - - + Admin + { diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/ShowHide.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/ShowHide.tsx new file mode 100644 index 0000000000..48235bad49 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/ShowHide.tsx @@ -0,0 +1,44 @@ +import { Box, IconButton, styled, Tooltip } from '@mui/material'; +import type { NavigationMode } from './NavigationMode'; +import type { FC } from 'react'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; + +const ShowHideWrapper = styled(Box, { + shouldForwardProp: (prop) => prop !== 'mode', +})<{ mode: NavigationMode }>(({ theme, mode }) => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + margin: theme.spacing(2, 1, 0, mode === 'mini' ? 1.5 : 2), + cursor: 'pointer', +})); + +export const ShowHide: FC<{ mode: NavigationMode; onChange: () => void }> = ({ + mode, + onChange, +}) => { + return ( + + {mode === 'full' && ( + ({ + color: theme.palette.neutral.main, + fontSize: 'small', + })} + > + Hide (⌘ + B) + + )} + + {mode === 'full' ? ( + + ) : ( + + + + )} + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/useExpanded.ts b/frontend/src/component/layout/MainLayout/NavigationSidebar/useExpanded.ts new file mode 100644 index 0000000000..2074dc857b --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/useExpanded.ts @@ -0,0 +1,19 @@ +import { useLocalStorageState } from 'hooks/useLocalStorageState'; +import { unique } from 'utils/unique'; + +export const useExpanded = () => { + const [expanded, setExpanded] = useLocalStorageState>( + 'navigation-expanded:v1', + [], + ); + + const changeExpanded = (key: T, expand: boolean) => { + if (expand) { + setExpanded(unique([...expanded, key])); + } else { + setExpanded(expanded.filter((name) => name !== key)); + } + }; + + return [expanded, changeExpanded] as const; +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/useNavigationMode.ts b/frontend/src/component/layout/MainLayout/NavigationSidebar/useNavigationMode.ts new file mode 100644 index 0000000000..894e6a750e --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/useNavigationMode.ts @@ -0,0 +1,26 @@ +import { useLocalStorageState } from 'hooks/useLocalStorageState'; +import type { NavigationMode } from './NavigationMode'; +import { useEffect } from 'react'; + +export const useNavigationMode = () => { + const [mode, setMode] = useLocalStorageState( + 'navigation-mode:v1', + 'full', + ); + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'b' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + setMode(mode === 'mini' ? 'full' : 'mini'); + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [mode]); + + return [mode, setMode] as const; +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/useRoutes.ts b/frontend/src/component/layout/MainLayout/NavigationSidebar/useRoutes.ts new file mode 100644 index 0000000000..abb3b38301 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/useRoutes.ts @@ -0,0 +1,22 @@ +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { getCondensedRoutes, getRoutes } from 'component/menu/routes'; +import { useAdminRoutes } from 'component/admin/useAdminRoutes'; +import { filterByConfig, mapRouteLink } from 'component/common/util'; + +export const useRoutes = () => { + const { uiConfig } = useUiConfig(); + const routes = getRoutes(); + const adminRoutes = useAdminRoutes(); + + const filteredMainRoutes = { + mainNavRoutes: getCondensedRoutes(routes.mainNavRoutes) + .filter(filterByConfig(uiConfig)) + .map(mapRouteLink), + mobileRoutes: getCondensedRoutes(routes.mobileRoutes) + .filter(filterByConfig(uiConfig)) + .map(mapRouteLink), + adminRoutes, + }; + + return { routes: filteredMainRoutes }; +};