aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/templates/layout/site-header/site-navbar.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-21 19:01:18 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-22 12:52:35 +0100
commitd4045fbcbfa8208ec31539744417f315f1f6fad8 (patch)
tree54746d3e28cc6e4a2d7d1e54a4b2e3e1e74a6896 /src/components/templates/layout/site-header/site-navbar.tsx
parentc6212f927daf3c928f479afa052e4772216a2d8a (diff)
refactor(components): split Layout component in smaller components
The previous component was too long and hardly readable. So I splitted it in different part and added tests.
Diffstat (limited to 'src/components/templates/layout/site-header/site-navbar.tsx')
-rw-r--r--src/components/templates/layout/site-header/site-navbar.tsx210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/components/templates/layout/site-header/site-navbar.tsx b/src/components/templates/layout/site-header/site-navbar.tsx
new file mode 100644
index 0000000..96aeb4f
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-navbar.tsx
@@ -0,0 +1,210 @@
+import { useRouter } from 'next/router';
+import {
+ type FormEvent,
+ useCallback,
+ type ForwardRefRenderFunction,
+ forwardRef,
+ useRef,
+} from 'react';
+import { useIntl } from 'react-intl';
+import { ROUTES } from '../../../../utils/constants';
+import { Icon } from '../../../atoms';
+import {
+ MainNav,
+ type MainNavItem,
+ Navbar,
+ SearchForm,
+ type SearchFormSubmit,
+ SettingsForm,
+ type NavbarProps,
+ NavbarItem,
+ type SearchFormRef,
+ type NavbarItemActivationHandler,
+} from '../../../organisms';
+import styles from './site-header.module.scss';
+
+export type SiteNavbarProps = Omit<NavbarProps, 'children'>;
+
+const SiteNavbarWithRef: ForwardRefRenderFunction<
+ HTMLUListElement,
+ SiteNavbarProps
+> = (props, ref) => {
+ const router = useRouter();
+ const intl = useIntl();
+ const labels = {
+ mainNavItem: intl.formatMessage({
+ defaultMessage: 'Open menu',
+ description: 'SiteNavbar: main nav button label in navbar',
+ id: '2By3AZ',
+ }),
+ mainNavModal: intl.formatMessage({
+ defaultMessage: 'Main navigation',
+ description: 'SiteNavbar: main nav accessible name',
+ id: 'QQAcaS',
+ }),
+ searchItem: intl.formatMessage({
+ defaultMessage: 'Open search',
+ id: 'Z/rsgm',
+ description: 'SiteNavbar: search button label in navbar',
+ }),
+ searchModal: intl.formatMessage({
+ defaultMessage: 'Search',
+ description: 'SiteNavbar: search modal title in navbar',
+ id: '5eq0+c',
+ }),
+ settingsItem: intl.formatMessage({
+ defaultMessage: 'Open settings',
+ id: 'l50cYa',
+ description: 'SiteNavbar: settings button label in navbar',
+ }),
+ settingsForm: intl.formatMessage({
+ defaultMessage: 'Settings form',
+ id: 'zhjPcZ',
+ description:
+ 'SiteNavbar: an accessible name for the settings form in navbar',
+ }),
+ settingsModal: intl.formatMessage({
+ defaultMessage: 'Settings',
+ description: 'SiteNavbar: settings modal title in navbar',
+ id: 'uKef8u',
+ }),
+ };
+ const mainNav: MainNavItem[] = [
+ {
+ id: 'home',
+ label: intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'SiteNavbar: main nav - home link',
+ id: 'PnrHgZ',
+ }),
+ href: '/',
+ // eslint-disable-next-line react/jsx-no-literals
+ logo: <Icon aria-hidden={true} shape="home" />,
+ },
+ {
+ id: 'blog',
+ label: intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'SiteNavbar: main nav - blog link',
+ id: '5C+1PP',
+ }),
+ href: ROUTES.BLOG,
+ // eslint-disable-next-line react/jsx-no-literals
+ logo: <Icon aria-hidden={true} shape="posts-stack" />,
+ },
+ {
+ id: 'projects',
+ label: intl.formatMessage({
+ defaultMessage: 'Projects',
+ description: 'SiteNavbar: main nav - projects link',
+ id: 'JXLaT8',
+ }),
+ href: ROUTES.PROJECTS,
+ // eslint-disable-next-line react/jsx-no-literals
+ logo: <Icon aria-hidden={true} shape="computer" />,
+ },
+ {
+ id: 'cv',
+ label: intl.formatMessage({
+ defaultMessage: 'CV',
+ description: 'SiteNavbar: main nav - cv link',
+ id: 'MJLr6U',
+ }),
+ href: ROUTES.CV,
+ // eslint-disable-next-line react/jsx-no-literals
+ logo: <Icon aria-hidden={true} shape="career" />,
+ },
+ {
+ id: 'contact',
+ label: intl.formatMessage({
+ defaultMessage: 'Contact',
+ description: 'SiteNavbar: main nav - contact link',
+ id: 'XGmQXV',
+ }),
+ href: ROUTES.CONTACT,
+ // eslint-disable-next-line react/jsx-no-literals
+ logo: <Icon aria-hidden={true} shape="envelop" />,
+ },
+ ];
+ const settingsSubmitHandler = useCallback((e: FormEvent) => {
+ e.preventDefault();
+ }, []);
+
+ const searchFormRef = useRef<SearchFormRef>(null);
+ const giveFocusToSearchInput: NavbarItemActivationHandler = useCallback(
+ (isActive) => {
+ if (isActive) searchFormRef.current?.focus();
+ },
+ []
+ );
+ const searchSubmitHandler: SearchFormSubmit = useCallback(
+ async ({ query }) => {
+ if (!query)
+ return {
+ messages: {
+ error: intl.formatMessage({
+ defaultMessage: 'Query must be longer than one character.',
+ description: 'SiteNavbar: invalid query message',
+ id: 'nRzO0T',
+ }),
+ },
+ validator: (value) => value.query.length > 1,
+ };
+
+ await router.push({ pathname: ROUTES.SEARCH, query: { s: query } });
+
+ return undefined;
+ },
+ [intl, router]
+ );
+
+ return (
+ <Navbar {...props} ref={ref}>
+ <NavbarItem
+ // eslint-disable-next-line react/jsx-no-literals
+ icon="hamburger"
+ // eslint-disable-next-line react/jsx-no-literals
+ id="main-nav"
+ label={labels.mainNavItem}
+ // eslint-disable-next-line react/jsx-no-literals
+ modalVisibleFrom="md"
+ >
+ <MainNav aria-label={labels.mainNavModal} items={mainNav} />
+ </NavbarItem>
+ <NavbarItem
+ activationHandlerDelay={300}
+ // eslint-disable-next-line react/jsx-no-literals
+ icon="magnifying-glass"
+ // eslint-disable-next-line react/jsx-no-literals
+ id="search"
+ label={labels.searchItem}
+ modalHeading={labels.searchModal}
+ onActivation={giveFocusToSearchInput}
+ >
+ <SearchForm
+ className={styles.search}
+ isLabelHidden
+ onSubmit={searchSubmitHandler}
+ ref={searchFormRef}
+ />
+ </NavbarItem>
+ <NavbarItem
+ // eslint-disable-next-line react/jsx-no-literals
+ icon="cog"
+ // eslint-disable-next-line react/jsx-no-literals
+ id="settings"
+ label={labels.settingsItem}
+ modalHeading={labels.settingsModal}
+ showIconOnModal
+ >
+ <SettingsForm
+ aria-label={labels.settingsForm}
+ className={styles.settings}
+ onSubmit={settingsSubmitHandler}
+ />
+ </NavbarItem>
+ </Navbar>
+ );
+};
+
+export const SiteNavbar = forwardRef(SiteNavbarWithRef);