Skip to content

Commit

Permalink
EPMGCIP-163: Implement Responsive Menu with SubMenus Using Mantine Li…
Browse files Browse the repository at this point in the history
…brary (#34)

* EPMGCIP-163: Implement basic layout for navigation header menu

* EPMGCIP-163: Implement base layout for desktop view for "Header" menu, add separate subcomponent for desktop view

* EPMGCIP-163: Add selected link highlight line in "Header" component

* EPMGCIP-163: Add new hook to track mobile view layout (sm breakpoint), update hierarchy for Header sub components

* EPMGCIP-163: Implement mobile layout and logic for sidebar (drawer), update styles definition

* EPMGCIP-163: Define a new component to contain element with sub links in for mobile, update usages

* EPMGCIP-163: Add support for props passing to "Header" component, update usages

* EPMGCIP-163: Improve logic with tracking selected link, add support for disabling nested link, update usages

* EPMGCIP-163: Update "Header" related components with "data-testid" attribute for testing purposes, extend types declaration to include custom props

* EPMGCIP-163: Improve UTs for "Header" to check new scenarios

* EPMGCIP-163: Add new component to store layout for sublinks items for desktop, update usages

* EPMGCIP-163: Rename "global.d.ts" to "custom-types.d.ts" to avoid confusions, update usages

* EPMGCIP-163: Disable extra error rule "react/react-in-jsx-scope" due to redundancy

* EPMGCIP-163: Implement UTs for "useMobileView"

* EPMGCIP-163: Make mobile link including sublinks clickable in "MobileSubLinks"

* EPMGCIP-163: Resolve desktop header dead zone issue on hover in styles

* EPMGCIP-163: Improve option with highlighted sublink in desktop header

* EPMGCIP-163: Replace hardcoded font-size values with variables in "Header.module.scss"

* EPMGCIP-163: Update usages of variables for "Header.module.scss" to make consistent flow

* EPMGCIP-163: Define set of UI theme breakpoints, update usage in "useMobileView"

* EPMGCIP-163: Remove some explicit function type in return in "Header" related components

* EPMGCIP-163: Define new theme color for menu link, update usage in "Header.module.scss"

* EPMGCIP-163: Update logic with tracking selected link on pathname change in "Header"
  • Loading branch information
Dzmitry-Yaniuk authored Jan 6, 2025
1 parent 7b29fc9 commit a1b9812
Show file tree
Hide file tree
Showing 25 changed files with 732 additions and 35 deletions.
7 changes: 7 additions & 0 deletions custom-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from '@mantine/core';

declare module '@mantine/core' {
export interface ModalBaseCloseButtonProps {
'data-testid'?: string | null;
}
}
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@mantine/modals": "^7.13.4",
"@mantine/notifications": "^7.13.4",
"@next/third-parties": "^14.2.5",
"@tabler/icons-react": "^3.26.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"next": "^14.2.5",
Expand Down
14 changes: 11 additions & 3 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import React from 'react';

import { ColorSchemeScript, createTheme, MantineProvider } from '@mantine/core';
import { Notifications } from '@mantine/notifications';
import { GoogleTagManager } from '@next/third-parties/google';
import type { Metadata } from 'next';
Expand All @@ -7,6 +9,8 @@ import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';

import Header from '@/components/organisms/Header/Header';
import { THEME_BREAKPOINTS } from '@/constants/breakpoints';
import { APP_ROUTES } from '@/constants/routes';

import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
Expand All @@ -19,6 +23,10 @@ const montserrat = Montserrat({
weight: ['400', '700'],
});

const theme = createTheme({
breakpoints: THEME_BREAKPOINTS,
});

export const metadata: Metadata = {
description: 'Museum',
title: 'Museum',
Expand All @@ -40,10 +48,10 @@ export default async function RootLayout({
</head>
<GoogleTagManager gtmId={process.env.CONTENTFUL_GTAG_ID || ''} />
<body>
<MantineProvider>
<MantineProvider theme={theme}>
<Notifications />
<NextIntlClientProvider messages={messages}>
<Header />
<Header links={APP_ROUTES} />
{children}
</NextIntlClientProvider>
</MantineProvider>
Expand Down
58 changes: 58 additions & 0 deletions src/components/organisms/Header/DesktopHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';

import { Group } from '@mantine/core';
import { clsx } from 'clsx';
import Image from 'next/image';
import { useTranslations } from 'next-intl';

import logo from '@/assets/image/logo.png';
import LanguageSwitcher from '@/components/molecules/LanguageSwitcher/LanguageSwitcher';
import { DesktopSubLinks } from '@/components/organisms/Header/DesktopSubLinks';
import { BASE_URL } from '@/constants/routes';
import { ILink } from '@/interfaces/ILink';
import { Link } from '@/navigation';

import styles from './Header.module.scss';

interface Props {
activeLinkIndex: number;
links: ILink[];
}

export const DesktopHeader: React.FC<Props> = (props) => {
const t = useTranslations();

return (
<>
<Link href={BASE_URL}>
<Image src={logo} width={68} alt={t('logo')} />
</Link>

<Group gap={50} className={styles.desktopContainer}>
{props.links.map((link, index) => {
const isSubMenuItem = !!link.subLinks;
const isSelectedLink = props.activeLinkIndex === index;

if (!isSubMenuItem) {
const linkUrl = link.url!;

return (
<Link
data-testid="link"
key={link.label}
href={linkUrl}
className={clsx(styles.desktopLink, { [styles.desktopActiveLink]: isSelectedLink })}
>
{link.label}
</Link>
);
}

return <DesktopSubLinks key={link.label} link={link} isSelected={isSelectedLink} />;
})}
</Group>

<LanguageSwitcher />
</>
);
};
52 changes: 52 additions & 0 deletions src/components/organisms/Header/DesktopSubLinks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';

import { HoverCard } from '@mantine/core';
import { clsx } from 'clsx';

import { ILink } from '@/interfaces/ILink';
import { Link } from '@/navigation';

import styles from './Header.module.scss';

interface Props {
link: ILink;
isSelected: boolean;
}

export const DesktopSubLinks: React.FC<Props> = (props) => {
const onClickSubMenuLink = (e: React.MouseEvent<HTMLAnchorElement>): void => {
e.preventDefault();
};

return (
<HoverCard>
<HoverCard.Target>
<Link
data-testid="link"
href={''}
onClick={onClickSubMenuLink}
className={clsx(styles.desktopLink, {
[styles.desktopActiveLink]: props.isSelected,
})}
>
{props.link.label}
</Link>
</HoverCard.Target>

<HoverCard.Dropdown className={styles.subLinksDropdown}>
<div className={styles.desktopSubLinksContainer}>
{props.link.subLinks!.map((subLink) => (
<Link
data-testid="sub-link"
key={subLink.label}
href={subLink.url}
className={styles.desktopSubLink}
>
{subLink.label}
</Link>
))}
</div>
</HoverCard.Dropdown>
</HoverCard>
);
};
132 changes: 131 additions & 1 deletion src/components/organisms/Header/Header.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@use 'sass:map';

@import '../../../variables.scss';

.header {
Expand All @@ -7,15 +8,144 @@
padding: 8px $indentMobilePageBorder;
border-bottom: 1px solid $mainBorderColor;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 60px;

@media (min-width: map.get($breakpoints, sm)) {
padding: 8px $indentTabletPageBorder;
padding: 1px $indentTabletPageBorder;
}
}

.logo {
width: 68px;
cursor: pointer;
}

.desktopContainer {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 0;
margin: 0 5%;
}

.baseLink {
color: $colorMenuLink;
text-decoration: none;
outline: none;
width: fit-content;
display: flex;
align-items: center;
position: relative;
}

.desktopLink {
@extend .baseLink;

font-size: $fontSizeLarge;
height: 100%;
}

.mobileLink {
@extend .baseLink;

font-size: $fontSizeXLarge;
height: 50px;
}

.desktopSubLink {
@extend .baseLink;

font-size: $fontSizeDefault;
font-weight: normal;
margin-bottom: 5px;

&:hover {
color: $colorLink;
}

&:last-child {
margin-bottom: 0;
}
}

.mobileSubLink {
@extend .baseLink;

font-size: $fontSizeLarge;
height: 40px;
}

.baseActiveLink {
color: $colorLink;
font-weight: 600;
}

.desktopActiveLink {
@extend .baseActiveLink;

&:after {
display: block;
content: '';
height: 3px;
width: 100%;
background-color: $colorLink;
position: absolute;
bottom: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
}

.mobileActiveLink {
@extend .baseActiveLink;
}

.subLinksDropdown {
top: 60px !important;
border: 1px solid $menuBorderColor;
min-width: 150px;
box-shadow: rgba(100, 100, 111, 0.2) 0 7px 29px 0;
}

.desktopSubLinksContainer {
margin: 0;
padding: 0;
}

.mobileSubLinksContainer {
margin-left: 15px;
padding: 0;
}

.drawer {
display: flex;
flex-direction: column;
}

.mobileSubLinksTab {
display: flex;
align-items: center;
width: fit-content;
}

.baseChevronIcon {
margin: 3px 0 0 5px;
}

.chevronIcon {
@extend .baseChevronIcon;

color: $colorMenuLink;
}

.activeChevronIcon {
@extend .baseChevronIcon;

color: $colorLink;
}
Loading

0 comments on commit a1b9812

Please sign in to comment.