diff --git a/frontend/components/Breadcrumbs.tsx b/frontend/components/Breadcrumbs.tsx index edf4e7cc6..7737ce2f9 100644 --- a/frontend/components/Breadcrumbs.tsx +++ b/frontend/components/Breadcrumbs.tsx @@ -13,6 +13,11 @@ import BreadcrumbsTranslations, { Breadcrumbs as BreadcrumbsTranslationType, } from "/translations/breadcrumbs" +const BreadcrumbContainer = styled("nav")` + margin: 0 auto; + width: 100vw; + max-width: 1920px; +` const BreadcrumbList = styled("ul")` list-style: none; overflow: hidden; @@ -131,7 +136,7 @@ export function Breadcrumbs() { } return ( - + ) } diff --git a/frontend/components/NewLayout/Header/Header.tsx b/frontend/components/NewLayout/Header/Header.tsx index 4ca63e73d..e1e7af4f0 100644 --- a/frontend/components/NewLayout/Header/Header.tsx +++ b/frontend/components/NewLayout/Header/Header.tsx @@ -2,7 +2,7 @@ import { AppBar, Slide, Toolbar, useScrollTrigger } from "@mui/material" import { styled } from "@mui/material/styles" import HyLogoIcon from "../Icons/HyLogo" -import { DesktopNavigationMenu, MobileNavigationMenu } from "../Navigation" +import { NavigationMenu } from "../Navigation" import LanguageSwitch from "./LanguageSwitch" import MoocLogo from "./MoocLogo" import { useTranslator } from "/hooks/useTranslator" @@ -139,8 +139,7 @@ function Header() { - - + diff --git a/frontend/components/NewLayout/Icons/CaretLeft.tsx b/frontend/components/NewLayout/Icons/CaretLeft.tsx new file mode 100644 index 000000000..dc8f48b78 --- /dev/null +++ b/frontend/components/NewLayout/Icons/CaretLeft.tsx @@ -0,0 +1,10 @@ +import { createSvgIcon } from "@mui/material/utils" + +const CaretLeftIcon = createSvgIcon( + + + , + "CaretLeft", +) + +export default CaretLeftIcon diff --git a/frontend/components/NewLayout/Navigation/BottomNavigation.tsx b/frontend/components/NewLayout/Navigation/BottomNavigation.tsx deleted file mode 100644 index 83a0ce7d1..000000000 --- a/frontend/components/NewLayout/Navigation/BottomNavigation.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { AppBar, AppBarProps, Toolbar } from "@mui/material" -import { styled } from "@mui/material/styles" - -import { NavigationLinks } from "/components/NewLayout/Navigation/NavigationLinks" - -const BottomNavigationContainer = styled((props: AppBarProps) => ( - -))( - ({ theme }) => ` - ${theme.breakpoints.up("md")} { - display: none; - } - top: auto; - margin: auto; - bottom: 0; -`, -) - -export const BottomNavigation = () => { - return ( - - - - - - ) -} diff --git a/frontend/components/NewLayout/Navigation/DesktopNavigationMenu.tsx b/frontend/components/NewLayout/Navigation/DesktopNavigationMenu.tsx index c57c64880..af7ac2e1a 100644 --- a/frontend/components/NewLayout/Navigation/DesktopNavigationMenu.tsx +++ b/frontend/components/NewLayout/Navigation/DesktopNavigationMenu.tsx @@ -14,6 +14,7 @@ import { } from "@mui/material" import { styled, Theme } from "@mui/material/styles" +import { NavigationMenuItem } from "." import { NavigationLinks } from "./NavigationLinks" import { useLoginStateContext } from "/contexts/LoginStateContext" import { useTranslator } from "/hooks/useTranslator" @@ -124,10 +125,14 @@ const UserOptionsMenu = () => { ) } -const DesktopNavigationMenu = () => { +interface DesktopNavigationMenuProps { + items: Array +} + +const DesktopNavigationMenu = ({ items }: DesktopNavigationMenuProps) => { return ( <> - + diff --git a/frontend/components/NewLayout/Navigation/MobileNavigationMenu.tsx b/frontend/components/NewLayout/Navigation/MobileNavigationMenu.tsx index 75c7d619a..a57c20299 100644 --- a/frontend/components/NewLayout/Navigation/MobileNavigationMenu.tsx +++ b/frontend/components/NewLayout/Navigation/MobileNavigationMenu.tsx @@ -1,38 +1,27 @@ -import React, { useState } from "react" +import React, { createContext, useMemo, useState } from "react" import { useRouter } from "next/router" -/*import { useApolloClient } from "@apollo/client" -import ChalkboardTeacher from "@fortawesome/fontawesome-free/svgs/solid/chalkboard-user.svg?icon" -import Dashboard from "@fortawesome/fontawesome-free/svgs/solid/gauge-high.svg?icon" -import ListIcon from "@fortawesome/fontawesome-free/svgs/solid/list.svg?icon" -import SignOut from "@fortawesome/fontawesome-free/svgs/solid/right-from-bracket.svg?icon" -import SignIn from "@fortawesome/fontawesome-free/svgs/solid/right-to-bracket.svg?icon" -import Register from "@fortawesome/fontawesome-free/svgs/solid/user-plus.svg?icon" -import User from "@fortawesome/fontawesome-free/svgs/solid/user.svg?icon"*/ import { Button, - Collapse, Drawer, EnhancedListItemButton, + ExtendList, IconButton, List, - ListItem, ListItemButton, - ListItemIcon, ListItemText, - SvgIcon, + ListTypeMap, } from "@mui/material" -import { styled } from "@mui/material/styles" +import { css, styled } from "@mui/material/styles" import { useEventCallback } from "@mui/material/utils" -import CaretDownIcon from "../Icons/CaretDown" -import CaretUpIcon from "../Icons/CaretUp" +import { isSubmenuItem, NavigationMenuItem } from "." +import CaretLeftIcon from "../Icons/CaretLeft" +import CaretRightIcon from "../Icons/CaretRight" import HamburgerIcon from "../Icons/Hamburger" import RemoveIcon from "../Icons/Remove" -import { useLoginStateContext } from "/contexts/LoginStateContext" import { useTranslator } from "/hooks/useTranslator" -// import { signOut } from "/lib/authentication" import CommonTranslations from "/translations/common" const MobileMenuContainer = styled("div")( @@ -58,32 +47,175 @@ const MobileMenu = styled(Drawer)( `, ) as typeof Drawer +const MobileMenuButton = styled(Button)( + ({ theme }) => ` + align-items: center; + display: inline-flex; + height: 100%; + justify-content: center; + width: 44px; + background-color: transparent; + border: 0; + padding: 0; + margin: 0; + text-transform: none; + + &[aria-expanded=true] { + font-size: 1rem; + line-height: 18px; + font-weight: 700; + align-items: center; + background-color: transparent; + border: none; + color: ${theme.palette.common.brand.main}; + display: inline-flex; + height: auto; + letter-spacing: -0.3px; + margin-left: auto; + margin-right: -16px; + padding: 16px; + position: fixed; + right: 16px; + top: 12px; + z-index: 1350; + width: auto; + + svg { + margin-left: 8px; + fill: ${theme.palette.common.brand.main}; + } + &:hover { + background-color: transparent; + cursor: pointer; + } + } +`, +) as typeof Button + const MobileMenuHeader = styled("section")` display: grid; grid-template-columns: 1fr auto; padding: 12px 80px 12px 16px; gap: 16px; - min-height: 68px; align-items: center; ` +interface MobileMenuLevelListProps { + isOpen?: boolean + level?: number +} -const MobileMenuList = styled(List)` - position: relative; -` as typeof List +const MobileMenuLevelList = styled(List, { + shouldForwardProp: (prop) => prop !== "isOpen" && prop !== "level", +})( + ({ theme, isOpen, level = 0 }) => ` + background-color: ${theme.palette.common.grayscale.white}; + display: block; + height: 80vh; + left: 0; + position: ${level > 0 ? "absolute" : "relative"}; + top: 0; + width: 100%; + z-index: ${1300 + level}; + ${ + level > 0 + ? css` + transform: translateX(100%); + transition: + transform 0.3s ease-in-out, + visibility 0.3s ease-in-out, + opacity 0.3s ease-in-out, + max-height 0.3s ease-in-out; + `.styles + : "transform: none;" + } + visibility: hidden !important; + max-height: 100vh; -const MobileMenuListItem = styled(ListItem)( - ({ theme }) => ` + ${ + isOpen && + css` + height: 80vh; + transform: none; + visibility: visible !important; + padding-bottom: 96px; + max-height: 100vh; + `.styles + } +`, +) as ExtendList> + +interface MobileMenuListItemProps { + isHidden?: boolean + isActive?: boolean + isSubmenu?: boolean +} + +const MobileMenuListItem = styled("li", { + shouldForwardProp: (prop) => prop !== "isHidden" && prop !== "isActive", +})( + ({ theme, isHidden, isActive, isSubmenu }) => ` margin: 0 0 4px; display: flex; padding: 0; + background-color: ${theme.palette.common.grayscale.white}; + transition: transform 0.3s ease-in-out, visibility 0.3s ease-in-out, opacity 0.3s ease-in-out; + ${isHidden ? `visibility: hidden !important;` : "visibility: visible;"}; + .MuiListItemText-primary { + svg { + color: ${ + isActive + ? theme.palette.common.grayscale.black + : theme.palette.common.brand.main + }; + } font-size: 1.3125rem; line-height: 28px; font-weight: 700; - color: ${theme.palette.common.brand.light}; + color: ${ + isActive + ? theme.palette.common.grayscale.black + : theme.palette.common.brand.light + }; letter-spacing: -0.42px; padding: 16px 0 16px 16px; + + ${ + isSubmenu + ? css` + font-size: 1rem; + line-height: 20px; + font-weight: 600; + align-items: center; + display: flex; + letter-spacing: -0.5px; + text-decoration: none; + width: 100%; + padding: 12px 16px; + color: ${isActive + ? theme.palette.common.grayscale.black + : theme.palette.common.brand.main}; + `.styles + : "" + } + } + + ${ + isActive + ? css` + position: relative; + &:before { + border-left: 3px solid ${theme.palette.common.grayscale.black}; + content: ""; + height: 75%; + left: 10px; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + } + `.styles + : "" } &.Mui-selected { @@ -95,76 +227,158 @@ const MobileMenuListItem = styled(ListItem)( `, ) -const MobileMenuListItemText = styled(ListItemText)( - ({ theme }) => ` - /*.MuiListItemText-primary { - font-size: 1.3125rem; - font-weight: 700; - color: ${theme.palette.common.brand.light}; - letter-spacing: -0.42px; - padding: 16px 0 16px 16px; - }*/ -`, -) as typeof ListItemText - const MobileMenuListItemButton = styled(ListItemButton)` + padding-top: 0; + padding-bottom: 0; &:hover { background: transparent; } ` as EnhancedListItemButton -const MobileMenuListItemIcon = styled(ListItemIcon)` - /* */ +const MobileMenuListItemSubmenuButton = styled(IconButton)( + ({ theme }) => ` + &:before { + background-color: ${theme.palette.common.grayscale.medium}; + width: 1px; + height: 32px; + content: ''; + display: inline-block; + transform: translateY(-50%); + top: 50%; + left: 0; + position: absolute; + } +`, +) + +const MobileMenuBreadcrumbs = styled("div")` + min-height: 44px; +` + +const MobileMenuBreadcrumbButton = styled(Button)( + ({ theme }) => ` + text-transform: none; + font-size: .9375rem; + line-height: 22px; + color: ${theme.palette.common.brand.light}; + align-items: center; + background-color: transparent; + border: 0 none; + cursor: pointer; + display: inline-flex; + text-decoration: none; + padding: 11px 0; + text-align: left; + + &:hover { + background-color: transparent; + } + + svg { + margin-right: 8px; + fill: ${theme.palette.common.grayscale.black}; + } +`, +) as typeof Button + +const MobileMenuContentContainer = styled("section")` + overflow: hidden; + position: relative; + min-height: calc(100vh - 126px); ` interface MobileMenuItemProps { - Icon?: typeof SvgIcon - href?: string - text: string - collapsable?: boolean + item: NavigationMenuItem + level?: number } -const MobileMenuItem = ({ - Icon, - href, - text, - collapsable, - children, -}: React.PropsWithChildren) => { +const MobileMenuItem = ({ item, level = 0 }: MobileMenuItemProps) => { + const { currentLevel, setCurrentLevel, setBreadcrumbs } = + useMobileMenuContext() const { pathname } = useRouter() - const [open, setOpen] = useState(false) + const hasSubmenu = isSubmenuItem(item) + const { href, label } = item const onClick = useEventCallback(() => { - if (collapsable) { - setOpen((prevOpen) => !prevOpen) - } + setBreadcrumbs((prev) => [...prev, item]) + setCurrentLevel(level + 1) }) return ( - - - {Icon && ( - - - - )} - - {collapsable && (open ? : )} + 0} + > + + {level > 0 && } + - {collapsable && {children}} + {hasSubmenu && ( + <> + + + + = level + 1} + > + + + + {item.items.map((subItem) => ( + + ))} + + + )} ) } -// const MobileNavigationMenu = forwardRef(({}, ref) => { -const MobileNavigationMenu = () => { +interface MobileMenuLevelProps { + level?: number +} + +const MobileMenuLevel = ({ + level = 0, + children, +}: React.PropsWithChildren) => { + const { currentLevel } = useMobileMenuContext() + + return ( + + {children} + + ) +} + +interface MobileNavigationMenuProps { + items: Array +} + +const MobileNavigationMenu = ({ items }: MobileNavigationMenuProps) => { const [open, setOpen] = useState(false) const t = useTranslator(CommonTranslations) - const { admin /*loggedIn, logInOrOut, currentUser*/ } = useLoginStateContext() - //const apollo = useApolloClient() + const [currentLevel, setCurrentLevel] = useState(0) + const [breadcrumbs, setBreadcrumbs] = useState< + Array + >([undefined]) - const onClick = useEventCallback(() => { + const onSubmenuClick = useEventCallback(() => { + setCurrentLevel((level) => level + 1) + }) + const onBackClick = useEventCallback(() => { + setCurrentLevel((level) => level - 1) + setBreadcrumbs((prev) => prev.slice(0, -1)) + }) + + const onMenuToggle = useEventCallback(() => { setOpen((prevOpen) => !prevOpen) }) @@ -172,122 +386,75 @@ const MobileNavigationMenu = () => { setOpen(false) }) - /*const onSignOut = useCallback(() => { - setOpen(false) - signOut(apollo, logInOrOut) - }, [apollo, signOut, logInOrOut]) - - const userDisplayName = useMemo(() => { - const name = currentUser?.full_name - - if (!name) { - return t("myProfile") - } - - return name - }, [currentUser, t])*/ - - /*const menuItems = useMemo(() => { - const items = [ - , - , - , - ] - - if (admin) { - items.push( - , - , - ) - } - if (loggedIn) { - items.push( - , - , - ) - } else { - items.push( - - {t("loginShort")} - , - - {t("signUp")} - , - ) - } - - return items - }, [loggedIn, onClose, t, admin, MobileMenuItemOld])*/ + const menuContextValue = useMemo( + () => ({ + currentLevel, + setCurrentLevel, + onSubmenuClick, + onBackClick, + breadcrumbs, + setBreadcrumbs, + }), + [currentLevel, breadcrumbs, setBreadcrumbs, onSubmenuClick, onBackClick], + ) return ( - - - - - - - - - - - {admin && } - + + + + + + + + {currentLevel > 0 && ( + + + {breadcrumbs.slice(-2)[0]?.label ?? "Main menu"} + + )} + + + {t("close")} + + + + + + {items.map((item) => ( + + ))} + + + ) } -//}) + +interface MobileMenuContextType { + currentLevel: number + setCurrentLevel: React.Dispatch> + onSubmenuClick: () => void + onBackClick: () => void + breadcrumbs: Array + setBreadcrumbs: React.Dispatch< + React.SetStateAction> + > +} + +const MobileMenuContext = createContext( + {} as MobileMenuContextType, +) +const useMobileMenuContext = () => React.useContext(MobileMenuContext) export default MobileNavigationMenu diff --git a/frontend/components/NewLayout/Navigation/NavigationDropdown.tsx b/frontend/components/NewLayout/Navigation/NavigationDropdown.tsx new file mode 100644 index 000000000..0ff2cc717 --- /dev/null +++ b/frontend/components/NewLayout/Navigation/NavigationDropdown.tsx @@ -0,0 +1,279 @@ +import { useState } from "react" + +import { + Button, + EnhancedButton, + EnhancedLink, + EnhancedLinkProps, + Link, + MenuItem, + MenuList, + Popover, +} from "@mui/material" +import { css, styled } from "@mui/material/styles" +import { useEventCallback } from "@mui/material/utils" + +import { NavigationLinkStyle } from "." +import ArrowRight from "../Icons/ArrowRight" +import CaretDownIcon from "../Icons/CaretDown" +import CaretRight from "../Icons/CaretRight" +import CaretUpIcon from "../Icons/CaretUp" + +interface NavigationDropdownButtonProps { + expanded?: boolean +} + +const NavigationDropdownButton = styled(Button, { + shouldForwardProp: (prop) => prop !== "expanded", +})( + ({ theme, expanded }) => ` + ${NavigationLinkStyle.styles} + position: relative; + margin: 0; + right: unset; + top: unset; + min-height: unset; + border-radius: 0; + color: ${theme.palette.common.brand.nearlyBlack}; + svg { + fill: ${theme.palette.common.brand.nearlyBlack}; + } + ${theme.breakpoints.up("xl")} { + font-size: 1rem; + line-height: 16px; + } + &:hover { + cursor: pointer; + color: ${theme.palette.common.brand.main}; + svg { + fill: ${theme.palette.common.brand.main}; + } + } + ${ + expanded + ? css` + &::after, + &::before { + background-color: ${theme.palette.common.grayscale.white}; + content: ""; + height: 5px; + width: 16px; + position: absolute; + bottom: -3px; + z-index: 3000 !important; + } + + &::before { + left: -16px; + } + + &::after { + right: -16px; + } + + box-shadow: inset 0 -4px 0 0 ${theme.palette.common.grayscale.black}; + `.styles + : "" + } +`, +) as EnhancedButton<"button", NavigationDropdownButtonProps> + +const NavigationDropdownMenu = styled(Popover)( + ({ theme }) => ` + left: -17px; + top: 2px; + + ${theme.breakpoints.up("lg")} { + top: 1px; + } + + .MuiPopover-paper { + width: max-content; + max-width: 120rem; + padding: 24px 16px 40px; + border: 1px solid ${theme.palette.common.grayscale.black}; + border-top: none; + border-radius: 0; + transition: none; + display: grid; + grid-template-columns: 1fr auto 1fr; + justify-items: center; + align-items: flex-start; + } +`, +) as typeof Popover + +const NavigationDropdownMenuPanelContainer = styled("div")` + align-items: baseline; + display: inline-flex; + flex-direction: column; + grid-column-start: 2; +` +const NavigationDropdownMenuList = styled(MenuList)` + /**/ +` + +const NavigationDropdownMenuHeader = styled("div")( + ({ theme }) => ` + display: inline-grid; + gap: 8px 16px; + grid-template-areas: 'icon link' 'icon description'; + grid-template-columns: auto; + grid-template-rows: auto; + margin-bottom: 16px; + align-items: flex-start; + + svg { + fill: ${theme.palette.common.grayscale.white}; + } +`, +) + +const NavigationDropdownContent = styled("div")` + padding-left: 56px; + display: inline-grid; + grid-template-columns: 310px; +` + +const NavigationDropdownMenuItem = styled(MenuItem)` + padding: 0; + :hover { + background: transparent; + } +` + +const NavigationDropdownMenuItemLink = styled(Link)( + ({ theme }) => ` + font-size: 1rem; + line-height: 24px; + font-weight: 600; + align-items: flex-start; + color: ${theme.palette.common.brand.main}; + display: inline-flex; + letter-spacing: -0.5px; + overflow: hidden; + text-decoration: none; + + :hover { + text-decoration: underline; + } + + svg { + margin-right: 12px; + margin-top: 8px; + fill: ${theme.palette.common.grayscale.black}; + } +`, +) as EnhancedLink + +const NavigationDropdownHeaderLink = styled(Link)` + font-size: 25px; + line-height: 32px; + font-weight: 700; + grid-area: link; + letter-spacing: -0.5px; + margin-top: 4px; + text-decoration: none; + width: max-content; + + :hover { + text-decoration: underline; + } +` as EnhancedLink + +const NavigationIconWrapper = styled("div")( + ({ theme }) => ` + background-color: ${theme.palette.common.grayscale.black}; + display: flex; + grid-area: icon; + height: 40px; + justify-content: center; + align-items: center; + width: 40px; +`, +) + +export const NavigationDropdownMenuLink = ({ + children, + ...props +}: EnhancedLinkProps) => ( + + + + {children} + + +) + +interface NavigationDropdownProps { + href?: string + label?: string + name?: string +} + +export const NavigationDropdownLink = ({ + name, + href, + label, + children, +}: React.PropsWithChildren) => { + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + + const onClick = useEventCallback( + (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + }, + ) + const onClose = useEventCallback(() => { + setAnchorEl(null) + }) + + const buttonName = name ?? label ?? "dropdown" + const menuName = `${buttonName}-menu` + + return ( + <> + + {label} + {open ? : } + + + + + + + + + {label} + + + + + {children} + + + + + + ) +} diff --git a/frontend/components/NewLayout/Navigation/NavigationLinks.tsx b/frontend/components/NewLayout/Navigation/NavigationLinks.tsx index 8aed7cbcf..1bd1f7882 100644 --- a/frontend/components/NewLayout/Navigation/NavigationLinks.tsx +++ b/frontend/components/NewLayout/Navigation/NavigationLinks.tsx @@ -1,26 +1,11 @@ -import { useState } from "react" - -import { - Button, - EnhancedButton, - EnhancedLink, - EnhancedLinkProps, - Link, - MenuItem, - MenuList, - Paper, - Popover, -} from "@mui/material" +import { EnhancedLink, Link } from "@mui/material" import { css, styled } from "@mui/material/styles" -import { useEventCallback } from "@mui/material/utils" -import ArrowRightIcon from "../Icons/ArrowRight" -import CaretDownIcon from "../Icons/CaretDown" -import CaretRight from "../Icons/CaretRight" -import CaretUpIcon from "../Icons/CaretUp" -import { useLoginStateContext } from "/contexts/LoginStateContext" -import { useTranslator } from "/hooks/useTranslator" -import CommonTranslations from "/translations/common" +import { isSubmenuItem, NavigationMenuItem } from "." +import { + NavigationDropdownLink, + NavigationDropdownMenuLink, +} from "./NavigationDropdown" const NavigationLinkStyle = css` font-size: 0.875rem; @@ -50,6 +35,7 @@ const NavigationLinkStyle = css` font-size: 10px; } ` + const NavigationLink = styled(Link)( ({ theme }) => ` ${NavigationLinkStyle.styles} @@ -71,99 +57,6 @@ const NavigationLink = styled(Link)( `, ) as EnhancedLink -interface NavigationDropdownButtonProps { - expanded?: boolean -} - -const NavigationDropdownButton = styled(Button, { - shouldForwardProp: (prop) => prop !== "expanded", -})( - ({ theme, expanded }) => ` - ${NavigationLinkStyle.styles} - position: relative; - margin: 0; - right: unset; - top: unset; - min-height: unset; - color: ${theme.palette.common.brand.nearlyBlack}; - svg { - fill: ${theme.palette.common.brand.nearlyBlack}; - } - ${theme.breakpoints.up("xl")} { - font-size: 1rem; - line-height: 16px; - } - &:hover { - cursor: pointer; - color: ${theme.palette.common.brand.main}; - svg { - fill: ${theme.palette.common.brand.main}; - } - } - ${ - expanded - ? css` - &::after, - &::before { - background-color: ${theme.palette.common.grayscale.white}; - content: ""; - height: 5px; - width: 16px; - position: absolute; - bottom: -1px; - z-index: 3000 !important; - } - - &::before { - left: -16px; - } - - &::after { - right: -16px; - } - - box-shadow: inset 0 -4px 0 0 ${theme.palette.common.grayscale.black}; - `.styles - : "" - } -`, -) as EnhancedButton<"button", NavigationDropdownButtonProps> - -const NavigationDropdownMenu = styled(Popover)` - left: -17px; - top: 1px; -` as typeof Popover - -const NavigationDropdownMenuPanel = styled(Paper)( - ({ theme }) => ` - position: relative; - width: max-content; - max-width: 120rem; - padding: 24px 16px 40px; - border-radius: 0; - background-color: ${theme.palette.common.grayscale.white}; - transition: none; - top: 1px; - display: grid; - grid-template-columns: 1fr auto 1fr; - justify-items: center; - align-items: flex-start; - border: 1px solid ${theme.palette.common.grayscale.black}; - border-top: none; - margin: 0 auto; -`, -) - -const NavigationDropdownMenuPanelContainer = styled("div")` - align-items: baseline; - display: inline-flex; - flex-direction: column; - grid-column-start: 2; -` -const NavigationDropdownMenuList = styled(MenuList)` - /**/ -` - const NavigationLinksContainer = styled("nav")( ({ theme }) => ` height: 100%; @@ -190,203 +83,40 @@ const NavigationLinkItem = styled("li")` position: relative; ` -const NavigationDropdownMenuHeader = styled("div")( - ({ theme }) => ` - display: inline-grid; - gap: 8px 16px; - grid-template-areas: 'icon link' 'icon description'; - grid-template-columns: auto; - grid-template-rows: auto; - margin-bottom: 16px; - align-items: flex-start; - - svg { - fill: ${theme.palette.common.grayscale.white}; - } -`, -) - -const NavigationIconWrapper = styled("div")( - ({ theme }) => ` - background-color: ${theme.palette.common.grayscale.black}; - display: flex; - grid-area: icon; - height: 40px; - justify-content: center; - align-items: center; - width: 40px; -`, -) - -const NavigationDropdownHeaderLink = styled(Link)` - font-size: 25px; - line-height: 32px; - font-weight: 700; - grid-area: link; - letter-spacing: -0.5px; - margin-top: 4px; - text-decoration: none; - width: max-content; - - :hover { - text-decoration: underline; - } -` as EnhancedLink - -const NavigationDropdownContent = styled("div")` - padding-left: 56px; - display: inline-grid; - grid-template-columns: 310px; -` - -const NavigationDropdownMenuItem = styled(MenuItem)` - padding: 0; -` - -const NavigationDropdownMenuItemLink = styled(Link)( - ({ theme }) => ` - font-size: 1rem; - line-height: 24px; - font-weight: 600; - align-items: flex-start; - color: ${theme.palette.common.brand.main}; - display: inline-flex; - letter-spacing: -0.5px; - overflow: hidden; - text-decoration: none; - - :hover { - text-decoration: underline; - } - - svg { - margin-right: 12px; - margin-top: 8px; - fill: ${theme.palette.common.grayscale.black}; - } -`, -) as EnhancedLink - -const NavigationDropdownMenuLink = ({ - children, - ...props -}: EnhancedLinkProps) => ( - - - - {children} - - -) -interface NavigationDropdownProps { - href?: string - label?: string - name?: string +interface NavigationItemProps { + item: NavigationMenuItem } -const NavigationDropdownLink = ({ - name, - href, - label, - children, -}: React.PropsWithChildren) => { - const [anchorEl, setAnchorEl] = useState(null) - const open = Boolean(anchorEl) - - const onClick = useEventCallback( - (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget) - }, - ) - const onClose = useEventCallback(() => { - setAnchorEl(null) - }) - - const buttonName = name ?? label ?? "dropdown" - const menuName = `${buttonName}-menu` - +const NavigationItem = ({ item }: NavigationItemProps) => { + const { name, label, href } = item return ( - <> - - {label} - {open ? : } - - - - - - - - - - {label} - - - - - {children} - - - - - - + + {isSubmenuItem(item) ? ( + + {item.items.map(({ name, label, href }) => ( + + {label} + + ))} + + ) : ( + {label} + )} + ) } -export const NavigationLinks = () => { - const { admin } = useLoginStateContext() - const t = useTranslator(CommonTranslations) +interface NavigationLinksProps { + items: Array +} + +export const NavigationLinks = ({ items }: NavigationLinksProps) => { return ( - - {t("courses")} - - - - - {t("modules")} - - - - {admin && ( - - - - {t("courses")} - - - {t("modules")} - - - {t("emailTemplates")} - - - {t("userSearch")} - - - - )} + {items.map((item) => ( + + ))} ) diff --git a/frontend/components/NewLayout/Navigation/NavigationMenu.tsx b/frontend/components/NewLayout/Navigation/NavigationMenu.tsx new file mode 100644 index 000000000..9e1d88e06 --- /dev/null +++ b/frontend/components/NewLayout/Navigation/NavigationMenu.tsx @@ -0,0 +1,66 @@ +import { useMemo } from "react" + +import { isDefined } from "remeda" + +import { + DesktopNavigationMenu, + MobileNavigationMenu, + NavigationMenuItem, +} from "." +import { useLoginStateContext } from "/contexts/LoginStateContext" +import { useTranslator } from "/hooks/useTranslator" +import CommonTranslations from "/translations/common" + +const NavigationMenu = () => { + const t = useTranslator(CommonTranslations) + const { admin } = useLoginStateContext() + + const items = + useMemo>( + () => + [ + { + label: t("courses"), + href: "/_new/courses", + }, + { + label: t("modules"), + href: "/_new/study-modules", + }, + admin + ? { + label: "Admin", + href: "/_new/admin", + items: [ + { + href: "/_new/admin/courses", + label: t("courses"), + }, + { + href: "/_new/admin/study-modules", + label: t("modules"), + }, + { + href: "/_new/admin/email-templates", + label: t("emailTemplates"), + }, + { + href: "/_new/admin/users/search", + label: t("userSearch"), + }, + ], + } + : null, + ].filter(isDefined), + [t, admin], + ) ?? [] + + return ( + <> + + + + ) +} + +export default NavigationMenu diff --git a/frontend/components/NewLayout/Navigation/index.tsx b/frontend/components/NewLayout/Navigation/index.tsx index c572424bb..5757f5e0d 100644 --- a/frontend/components/NewLayout/Navigation/index.tsx +++ b/frontend/components/NewLayout/Navigation/index.tsx @@ -1,5 +1,35 @@ import { useRouter } from "next/router" +import { css } from "@mui/material/styles" + +export const NavigationLinkStyle = css` + font-size: 0.875rem; + line-height: 16px; + font-weight: 700; + background-color: transparent; + border: none; + cursor: pointer; + display: flex; + flex-direction: row; + height: 100%; + letter-spacing: -0.7px; + padding: 26px 10px; + width: auto; + text-transform: uppercase; + text-decoration: none; + text-align: left; + align-items: center; + justify-content: center; + transition: 0.1s; + + svg { + pointer-events: none; + margin-left: 4px; + height: 10px; + width: 10px; + font-size: 10px; + } +` export function useActiveTab() { const { pathname } = useRouter() @@ -8,6 +38,28 @@ export function useActiveTab() { )?.[1] } +export type NavigationMenuLinkItem = { + href: string + label: string + name?: string + description?: string + level?: number + parent?: NavigationMenuItem +} + +export type NavigationMenuSubmenuItem = NavigationMenuLinkItem & { + items: Array +} + +export type NavigationMenuItem = + | NavigationMenuLinkItem + | NavigationMenuSubmenuItem + +export const isSubmenuItem = ( + item: NavigationMenuItem, +): item is NavigationMenuSubmenuItem => + Boolean((item as NavigationMenuSubmenuItem).items) + export { default as MobileNavigationMenu } from "./MobileNavigationMenu" export { default as DesktopNavigationMenu } from "./DesktopNavigationMenu" -export * from "./BottomNavigation" +export { default as NavigationMenu } from "./NavigationMenu" diff --git a/frontend/components/SkipLink.tsx b/frontend/components/SkipLink.tsx index 902ff5672..a6e2a4c16 100644 --- a/frontend/components/SkipLink.tsx +++ b/frontend/components/SkipLink.tsx @@ -5,11 +5,11 @@ import { useTranslator } from "/hooks/useTranslator" import CommonTranslations from "/translations/common" const SkipLinkContainer = styled("a")` - left: -999; + left: -999px; position: absolute; top: auto; - height: 1; - width: 1; + height: 1px; + width: 1px; overflow: hidden; z-index: -999; diff --git a/frontend/pages/_new/_layout.tsx b/frontend/pages/_new/_layout.tsx index 4cd3b5602..58acea060 100644 --- a/frontend/pages/_new/_layout.tsx +++ b/frontend/pages/_new/_layout.tsx @@ -8,7 +8,6 @@ import { Breadcrumbs } from "/components/Breadcrumbs" import Footer from "/components/Footer" import Alerts from "/components/HeaderBar/Alerts" import Header from "/components/NewLayout/Header" -import { BottomNavigation } from "/components/NewLayout/Navigation/BottomNavigation" import PageLoadingIndicators from "/components/PageLoadingIndicators" import SkipLink from "/components/SkipLink" @@ -21,6 +20,11 @@ const FooterDownPusherWrapper = styled("div")` const MainContent = styled("main")` position: relative; + display: flex; + flex-direction: column; + width: 100%; + max-width: 1920px; + margin: 0 auto; ` const Layout: React.FunctionComponent = ({ @@ -44,7 +48,6 @@ const Layout: React.FunctionComponent = ({ {children}