aboutsummaryrefslogtreecommitdiffstats
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
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.
-rw-r--r--src/components/organisms/forms/search-form/search-form.module.scss6
-rw-r--r--src/components/organisms/forms/search-form/search-form.tsx119
-rw-r--r--src/components/templates/layout/layout.module.scss133
-rw-r--r--src/components/templates/layout/layout.stories.tsx2
-rw-r--r--src/components/templates/layout/layout.test.tsx11
-rw-r--r--src/components/templates/layout/layout.tsx412
-rw-r--r--src/components/templates/layout/site-footer/index.ts1
-rw-r--r--src/components/templates/layout/site-footer/site-footer.module.scss42
-rw-r--r--src/components/templates/layout/site-footer/site-footer.test.tsx20
-rw-r--r--src/components/templates/layout/site-footer/site-footer.tsx93
-rw-r--r--src/components/templates/layout/site-header/index.ts2
-rw-r--r--src/components/templates/layout/site-header/site-branding.test.tsx23
-rw-r--r--src/components/templates/layout/site-header/site-branding.tsx91
-rw-r--r--src/components/templates/layout/site-header/site-header.module.scss90
-rw-r--r--src/components/templates/layout/site-header/site-header.test.tsx11
-rw-r--r--src/components/templates/layout/site-header/site-header.tsx25
-rw-r--r--src/components/templates/layout/site-header/site-navbar.test.tsx73
-rw-r--r--src/components/templates/layout/site-header/site-navbar.tsx210
-rw-r--r--src/i18n/en.json126
-rw-r--r--src/i18n/fr.json136
-rw-r--r--src/styles/base/_base.scss53
-rw-r--r--src/styles/base/_spacings.scss1
-rw-r--r--src/styles/base/_typography.scss53
-rw-r--r--src/styles/globals.scss1
-rw-r--r--src/utils/constants.ts1
25 files changed, 978 insertions, 757 deletions
diff --git a/src/components/organisms/forms/search-form/search-form.module.scss b/src/components/organisms/forms/search-form/search-form.module.scss
index db247a2..3edaef6 100644
--- a/src/components/organisms/forms/search-form/search-form.module.scss
+++ b/src/components/organisms/forms/search-form/search-form.module.scss
@@ -37,7 +37,7 @@
}
}
-.wrapper {
+.form {
display: flex;
&--no-label {
@@ -76,3 +76,7 @@
}
}
}
+
+.notice {
+ margin-top: var(--spacing-sm);
+}
diff --git a/src/components/organisms/forms/search-form/search-form.tsx b/src/components/organisms/forms/search-form/search-form.tsx
index 3d0efa2..a803d8c 100644
--- a/src/components/organisms/forms/search-form/search-form.tsx
+++ b/src/components/organisms/forms/search-form/search-form.tsx
@@ -4,17 +4,11 @@ import {
useId,
useImperativeHandle,
useRef,
+ type HTMLAttributes,
} from 'react';
import { useIntl } from 'react-intl';
import { type FormSubmitHandler, useForm } from '../../../../utils/hooks';
-import {
- Button,
- Form,
- type FormProps,
- Icon,
- Input,
- Label,
-} from '../../../atoms';
+import { Button, Form, Icon, Input, Label, Notice } from '../../../atoms';
import { LabelledField } from '../../../molecules';
import styles from './search-form.module.scss';
@@ -22,7 +16,10 @@ export type SearchFormData = { query: string };
export type SearchFormSubmit = FormSubmitHandler<SearchFormData>;
-export type SearchFormProps = Omit<FormProps, 'children' | 'onSubmit'> & {
+export type SearchFormProps = Omit<
+ HTMLAttributes<HTMLDivElement>,
+ 'children' | 'onSubmit'
+> & {
/**
* Should the label be visually hidden?
*
@@ -45,19 +42,18 @@ export type SearchFormRef = {
const SearchFormWithRef: ForwardRefRenderFunction<
SearchFormRef,
SearchFormProps
-> = ({ className = '', isLabelHidden = false, onSubmit, ...props }, ref) => {
+> = ({ isLabelHidden = false, onSubmit, ...props }, ref) => {
const intl = useIntl();
- const { values, submit, submitStatus, update } = useForm<SearchFormData>({
- initialValues: { query: '' },
- submitHandler: onSubmit,
- });
+ const { messages, submit, submitStatus, update, values } =
+ useForm<SearchFormData>({
+ initialValues: { query: '' },
+ submitHandler: onSubmit,
+ });
const id = useId();
const inputRef = useRef<HTMLInputElement>(null);
- const formClass = [
- styles.wrapper,
- styles[isLabelHidden ? 'wrapper--no-label' : 'wrapper--has-label'],
- className,
- ].join(' ');
+ const formClass = `${styles.form} ${
+ styles[isLabelHidden ? 'form--no-label' : 'form--has-label']
+ }`;
const labels = {
button: intl.formatMessage({
defaultMessage: 'Search',
@@ -84,48 +80,55 @@ const SearchFormWithRef: ForwardRefRenderFunction<
);
return (
- <Form {...props} className={formClass} onSubmit={submit}>
- <LabelledField
- className={styles.field}
- field={
- <Input
- className={styles.input}
- id={id}
+ <div {...props}>
+ <Form className={formClass} onSubmit={submit}>
+ <LabelledField
+ className={styles.field}
+ field={
+ <Input
+ className={styles.input}
+ id={id}
+ // eslint-disable-next-line react/jsx-no-literals
+ name="query"
+ onChange={update}
+ ref={inputRef}
+ // eslint-disable-next-line react/jsx-no-literals
+ type="search"
+ value={values.query}
+ />
+ }
+ label={
+ <Label htmlFor={id} isHidden={isLabelHidden}>
+ {labels.field}
+ </Label>
+ }
+ />
+ <Button
+ aria-label={labels.button}
+ className={styles.btn}
+ isLoading={submitStatus === 'PENDING'}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="neutral"
+ // eslint-disable-next-line react/jsx-no-literals
+ shape="initial"
+ type="submit"
+ >
+ <Icon
+ aria-hidden
+ className={styles.icon}
// eslint-disable-next-line react/jsx-no-literals
- name="query"
- onChange={update}
- ref={inputRef}
+ shape="magnifying-glass"
// eslint-disable-next-line react/jsx-no-literals
- type="search"
- value={values.query}
+ size="lg"
/>
- }
- label={
- <Label htmlFor={id} isHidden={isLabelHidden}>
- {labels.field}
- </Label>
- }
- />
- <Button
- aria-label={labels.button}
- className={styles.btn}
- isLoading={submitStatus === 'PENDING'}
- // eslint-disable-next-line react/jsx-no-literals
- kind="neutral"
- // eslint-disable-next-line react/jsx-no-literals
- shape="initial"
- type="submit"
- >
- <Icon
- aria-hidden
- className={styles.icon}
- // eslint-disable-next-line react/jsx-no-literals
- shape="magnifying-glass"
- // eslint-disable-next-line react/jsx-no-literals
- size="lg"
- />
- </Button>
- </Form>
+ </Button>
+ </Form>
+ {messages?.error && submitStatus === 'FAILED' ? (
+ <Notice className={styles.notice} kind="error">
+ {messages.error}
+ </Notice>
+ ) : null}
+ </div>
);
};
diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss
index 69c4ef0..cf2a10f 100644
--- a/src/components/templates/layout/layout.module.scss
+++ b/src/components/templates/layout/layout.module.scss
@@ -2,132 +2,10 @@
@use "../../../styles/abstracts/mixins" as mix;
@use "../../../styles/abstracts/placeholders";
-%typing-animation {
- --typing-animation: none;
-
- width: fit-content;
- position: relative;
- overflow: hidden;
-
- &::after {
- content: "|";
- display: block;
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- right: 0;
- background: var(--color-bg);
- color: var(--color-primary-darker);
- font-weight: 400;
- text-align: left;
- visibility: hidden;
- transform: translateX(100%);
- transform-origin: right;
- animation: var(--typing-animation);
-
- :global {
- animation: var(--typing-animation);
- }
- }
-}
-
-.header {
- display: grid;
- grid-template-columns:
- minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 100ch)
- minmax(0, 1fr);
- align-items: center;
- padding: var(--spacing-md) 0 var(--spacing-lg);
- border-bottom: fun.convert-px(3) solid var(--color-border-light);
-
- &__body {
- grid-column: 2;
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-md);
- }
-}
-
-.brand {
- &__logo {
- --logo-size: #{clamp(
- fun.convert-px(95),
- calc(120px - 5vw),
- fun.convert-px(120)
- )};
-
- animation: flip-logo 9s ease-in 0s 1;
- }
-
- &__title {
- font-size: var(--font-size-2xl);
-
- @extend %typing-animation;
- }
-
- &__baseline {
- color: var(--color-fg-light);
- font-size: var(--font-size-lg);
- font-weight: 600;
-
- @extend %typing-animation;
- }
-}
-
-.search,
-.settings {
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- min-width: 30ch;
- }
- }
-}
-
.main {
flex: 1;
}
-.footer {
- display: flex;
- flex-flow: column wrap;
- gap: var(--spacing-xs);
- place-items: center;
- place-content: center;
- padding: var(--spacing-md) 0 calc(var(--toolbar-size) + var(--spacing-md));
- border-top: fun.convert-px(3) solid var(--color-border-light);
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- --toolbar-size: 0px;
-
- flex-flow: row wrap;
- font-size: var(--font-size-sm);
- }
- }
-}
-
-.back-to-top {
- position: fixed;
- bottom: calc(var(--toolbar-size, 0px) + var(--spacing-md));
- right: var(--spacing-md);
- transition: all 0.4s ease-in 0s;
-
- &--hidden {
- opacity: 0;
- transform: translateY(calc(var(--button-height) + var(--spacing-md)));
- visibility: hidden;
- }
-
- &--visible {
- opacity: 1;
- transform: translateY(0);
- visibility: visible;
- }
-}
-
.noscript {
padding: var(--spacing-xs) var(--spacing-sm);
position: fixed;
@@ -153,14 +31,3 @@
}
}
}
-
-@keyframes flip-logo {
- 0%,
- 90% {
- transform: rotateY(180deg);
- }
-
- 100% {
- transform: rotateY(0deg);
- }
-}
diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx
index 67ad008..6d55f34 100644
--- a/src/components/templates/layout/layout.stories.tsx
+++ b/src/components/templates/layout/layout.stories.tsx
@@ -1,4 +1,4 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { Layout as LayoutComponent } from './layout';
/**
diff --git a/src/components/templates/layout/layout.test.tsx b/src/components/templates/layout/layout.test.tsx
index d3abe1d..43b94f4 100644
--- a/src/components/templates/layout/layout.test.tsx
+++ b/src/components/templates/layout/layout.test.tsx
@@ -1,6 +1,6 @@
import { describe, expect, it } from '@jest/globals';
import { render, screen as rtlScreen } from '../../../../tests/utils';
-import { Layout } from './layout';
+import { Layout, getLayout } from './layout';
const body =
'Sit dolorem eveniet. Sit sit odio nemo vitae corrupti modi sint est rerum. Pariatur quidem maiores distinctio. Quia et illum aspernatur est cum.';
@@ -33,3 +33,12 @@ describe('Layout', () => {
expect(rtlScreen.getByText(body)).toBeInTheDocument();
});
});
+
+describe('getLayout', () => {
+ it('wraps the given contents in a layout component', () => {
+ const PageContents = <div>{body}</div>;
+ const Page = getLayout(PageContents);
+
+ expect(Page.props).toStrictEqual({ children: PageContents });
+ });
+});
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
index ce7f1fa..4dfe5f3 100644
--- a/src/components/templates/layout/layout.tsx
+++ b/src/components/templates/layout/layout.tsx
@@ -1,67 +1,24 @@
-/* eslint-disable max-statements */
-import NextImage from 'next/image';
-import { useRouter } from 'next/router';
import Script from 'next/script';
-import {
- type FC,
- type ReactElement,
- type ReactNode,
- useRef,
- type CSSProperties,
- type FormEvent,
- useCallback,
-} from 'react';
+import type { FC, ReactElement, ReactNode } from 'react';
import { useIntl } from 'react-intl';
import type { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
import type { NextPageWithLayoutOptions } from '../../../types';
import { CONFIG } from '../../../utils/config';
import { ROUTES } from '../../../utils/constants';
-import { useOnRouteChange, useScrollPosition } from '../../../utils/hooks';
-import {
- ButtonLink,
- Footer,
- Header,
- Heading,
- Icon,
- Logo,
- Main,
-} from '../../atoms';
-import {
- BackToTop,
- Branding,
- Colophon,
- type ColophonLink,
- Copyright,
- FlippingLogo,
-} from '../../molecules';
-import {
- type MainNavItem,
- Navbar,
- MainNav,
- SearchForm,
- SettingsForm,
- type SearchFormSubmit,
- NavbarItem,
- type SearchFormRef,
- type NavbarItemActivationHandler,
-} from '../../organisms';
+import { ButtonLink, Main } from '../../atoms';
import styles from './layout.module.scss';
+import { SiteFooter } from './site-footer';
+import { SiteHeader, type SiteHeaderProps } from './site-header';
export type QueryAction = SearchAction & {
'query-input': string;
};
-export type LayoutProps = {
+export type LayoutProps = Pick<SiteHeaderProps, 'isHome'> & {
/**
* The layout main content.
*/
children: ReactNode;
- /**
- * Is it homepage?
- *
- * @default false
- */
- isHome?: boolean;
};
/**
@@ -70,187 +27,22 @@ export type LayoutProps = {
* Render the base layout used by all pages.
*/
export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
- const router = useRouter();
- const intl = useIntl();
const { baseline, copyright, locales, name, url } = CONFIG;
-
- const skipToContent = intl.formatMessage({
- defaultMessage: 'Skip to content',
- description: 'Layout: Skip to content link',
- id: 'K4rYdT',
- });
- const noScript = intl.formatMessage({
- defaultMessage:
- 'Warning: If you want to benefit from all features (search for example), please activate Javascript.',
- description: 'Layout: noscript message',
- id: '7jVUT6',
- });
- const copyrightTitle = intl.formatMessage({
- defaultMessage: 'CC BY SA',
- description: 'Layout: copyright title',
- id: 'yB1SPF',
- });
-
- const homeLabel = intl.formatMessage({
- defaultMessage: 'Home',
- description: 'Layout: main nav - home link',
- id: 'bojYF5',
- });
- const blogLabel = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'Layout: main nav - blog link',
- id: 'D8vB38',
- });
- const projectsLabel = intl.formatMessage({
- defaultMessage: 'Projects',
- description: 'Layout: main nav - projects link',
- id: 'qnwsWV',
- });
- const cvLabel = intl.formatMessage({
- defaultMessage: 'CV',
- description: 'Layout: main nav - cv link',
- id: 'R895yC',
- });
- const contactLabel = intl.formatMessage({
- defaultMessage: 'Contact',
- description: 'Layout: main nav - contact link',
- id: 'AE4kCD',
- });
- const photoAltText = intl.formatMessage(
- {
- defaultMessage: '{website} picture',
- description: 'Layout: photo alternative text',
- id: '8jjY1X',
- },
- { website: name }
- );
- const logoTitle = intl.formatMessage(
- {
- defaultMessage: '{website} logo',
- description: 'Layout: logo title',
- id: '52H2HA',
- },
- { website: name }
- );
- const backToTop = intl.formatMessage({
- defaultMessage: 'Back to top',
- description: 'Layout: an accessible name for the back to top button',
- id: 'Kjj1Zk',
- });
-
- const mainNav: MainNavItem[] = [
- {
- id: 'home',
- label: homeLabel,
- href: '/',
- logo: <Icon aria-hidden={true} shape="home" />,
- },
- {
- id: 'blog',
- label: blogLabel,
- href: ROUTES.BLOG,
- logo: <Icon aria-hidden={true} shape="posts-stack" />,
- },
- {
- id: 'projects',
- label: projectsLabel,
- href: ROUTES.PROJECTS,
- logo: <Icon aria-hidden={true} shape="computer" />,
- },
- {
- id: 'cv',
- label: cvLabel,
- href: ROUTES.CV,
- logo: <Icon aria-hidden={true} shape="career" />,
- },
- {
- id: 'contact',
- label: contactLabel,
- href: ROUTES.CONTACT,
- logo: <Icon aria-hidden={true} shape="envelop" />,
- },
- ];
-
- const labels = {
- mainNavItem: intl.formatMessage({
- defaultMessage: 'Open menu',
- description: 'Layout: main nav button label in navbar',
- id: 'Fgt/RZ',
- }),
- mainNavModal: intl.formatMessage({
- defaultMessage: 'Main navigation',
- description: 'Layout: main nav accessible name',
- id: 'dfTljv',
- }),
- searchItem: intl.formatMessage({
- defaultMessage: 'Open search',
- id: 'XRwEoA',
- description: 'Layout: search button label in navbar',
- }),
- searchModal: intl.formatMessage({
- defaultMessage: 'Search',
- description: 'Layout: search modal title in navbar',
- id: 'Mq+O6q',
- }),
- settingsItem: intl.formatMessage({
- defaultMessage: 'Open settings',
- id: 'mDKiaN',
- description: 'Layout: settings button label in navbar',
- }),
- settingsForm: intl.formatMessage({
- defaultMessage: 'Settings form',
- id: 'h3J0a+',
- description: 'Layout: an accessible name for the settings form in navbar',
+ const intl = useIntl();
+ const messages = {
+ noScript: intl.formatMessage({
+ defaultMessage:
+ 'Warning: If you want to benefit from all features (search for example), please activate Javascript.',
+ description: 'Layout: noscript message',
+ id: '7jVUT6',
}),
- settingsModal: intl.formatMessage({
- defaultMessage: 'Settings',
- description: 'Layout: settings modal title in navbar',
- id: 'o3WSz5',
+ skipToContent: intl.formatMessage({
+ defaultMessage: 'Skip to content',
+ description: 'Layout: Skip to content link',
+ id: 'K4rYdT',
}),
};
- 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(
- ({ query }) => {
- if (!query)
- return {
- messages: {
- error: intl.formatMessage({
- defaultMessage: 'Query must be longer than one character.',
- description: 'Layout: invalid query message',
- id: 'C2YcUJ',
- }),
- },
- validator: (value) => value.query.length > 1,
- };
-
- router.push({ pathname: ROUTES.SEARCH, query: { s: query } });
-
- return undefined;
- },
- [intl, router]
- );
-
- const legalNoticeLabel = intl.formatMessage({
- defaultMessage: 'Legal notice',
- description: 'Layout: Legal notice label',
- id: 'nwbzKm',
- });
-
- const footerNav: ColophonLink[] = [
- { id: 'legal-notice', label: legalNoticeLabel, href: ROUTES.LEGAL_NOTICE },
- ];
-
const searchActionSchema: QueryAction = {
'@type': 'SearchAction',
target: {
@@ -260,7 +52,14 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
query: 'required',
'query-input': 'required name=search_term_string',
};
-
+ const brandingSchema: Person = {
+ '@type': 'Person',
+ name,
+ url,
+ jobTitle: baseline,
+ image: '/armand-philippot.jpg',
+ subjectOf: { '@id': `${url}` },
+ };
const schemaJsonLd: WithContext<WebSite> = {
'@context': 'https://schema.org',
'@id': `${url}`,
@@ -268,51 +67,16 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
name,
description: baseline,
url,
- author: { '@id': `${url}/#branding` },
+ author: brandingSchema,
copyrightYear: Number(copyright.startYear),
- creator: { '@id': `${url}/#branding` },
- editor: { '@id': `${url}/#branding` },
+ creator: brandingSchema,
+ editor: brandingSchema,
inLanguage: locales.defaultLocale,
potentialAction: searchActionSchema,
};
- const brandingSchema: WithContext<Person> = {
- '@context': 'https://schema.org',
- '@type': 'Person',
- '@id': `${url}/#branding`,
- name,
- url,
- jobTitle: baseline,
- image: '/armand-philippot.jpg',
- subjectOf: { '@id': `${url}` },
- };
-
- const scrollPos = useScrollPosition();
- const backToTopBreakpoint = 300;
- const backToTopClassName = [
- styles['back-to-top'],
- styles[
- scrollPos.y > backToTopBreakpoint
- ? 'back-to-top--visible'
- : 'back-to-top--hidden'
- ],
- ].join(' ');
-
- const topRef = useRef<HTMLSpanElement>(null);
- const giveFocusToTopRef = () => {
- if (topRef.current) topRef.current.focus();
- };
-
- useOnRouteChange(giveFocusToTopRef);
-
- const brandingTitleStyles = {
- '--typing-animation':
- 'blink 0.7s ease-in-out 0s 2, typing 4.3s linear 0s 1',
- } as CSSProperties;
- const brandingBaselineStyles = {
- '--typing-animation':
- 'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1',
- } as CSSProperties;
+ const topId = 'top';
+ const mainId = 'main';
return (
<>
@@ -322,119 +86,25 @@ export const Layout: FC<LayoutProps> = ({ children, isHome }) => {
id="schema-layout"
type="application/ld+json"
/>
- <Script
- dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }}
- // eslint-disable-next-line react/jsx-no-literals -- Id allowed
- id="schema-branding"
- type="application/ld+json"
- />
+ <span id={topId} />
<noscript>
<div className={styles['noscript-spacing']} />
</noscript>
- <span ref={topRef} tabIndex={-1} />
- <ButtonLink className="screen-reader-text" to="#main">
- {skipToContent}
+ <ButtonLink
+ // eslint-disable-next-line react/jsx-no-literals
+ className="screen-reader-text"
+ // eslint-disable-next-line react/jsx-no-literals
+ to={`#${mainId}`}
+ >
+ {messages.skipToContent}
</ButtonLink>
- <Header className={styles.header}>
- <div className={styles.header__body}>
- <Branding
- baseline={
- <div
- className={styles.brand__baseline}
- style={brandingBaselineStyles}
- >
- {baseline}
- </div>
- }
- logo={
- <FlippingLogo
- back={<Logo heading={logoTitle} />}
- className={styles.brand__logo}
- front={
- <NextImage
- alt={photoAltText}
- height={120}
- src="/armand-philippot.jpg"
- width={120}
- />
- }
- />
- }
- name={
- <Heading
- className={styles.brand__title}
- isFake={!isHome}
- level={1}
- style={brandingTitleStyles}
- >
- {name}
- </Heading>
- }
- url="/"
- />
- <Navbar>
- <NavbarItem
- icon="hamburger"
- id="main-nav"
- label={labels.mainNavItem}
- modalVisibleFrom="md"
- >
- <MainNav aria-label={labels.mainNavModal} items={mainNav} />
- </NavbarItem>
- <NavbarItem
- activationHandlerDelay={350}
- icon="magnifying-glass"
- id="search"
- label={labels.searchItem}
- modalHeading={labels.searchModal}
- onActivation={giveFocusToSearchInput}
- >
- <SearchForm
- className={styles.search}
- isLabelHidden
- onSubmit={searchSubmitHandler}
- ref={searchFormRef}
- />
- </NavbarItem>
- <NavbarItem
- icon="cog"
- id="settings"
- label={labels.settingsItem}
- modalHeading={labels.settingsModal}
- showIconOnModal
- >
- <SettingsForm
- aria-label={labels.settingsForm}
- className={styles.settings}
- onSubmit={settingsSubmitHandler}
- />
- </NavbarItem>
- </Navbar>
- </div>
- </Header>
- <Main id="main" className={styles.main}>
+ <SiteHeader className={styles.header} isHome={isHome} />
+ <Main className={styles.main} id={mainId}>
{children}
</Main>
- <Footer className={styles.footer}>
- <Colophon
- copyright={
- <Copyright
- from={copyright.startYear}
- owner={name}
- to={copyright.endYear}
- />
- }
- license={<Icon heading={copyrightTitle} shape="cc-by-sa" size="lg" />}
- links={footerNav}
- />
- <BackToTop
- anchor="#top"
- className={backToTopClassName}
- label={backToTop}
- />
- </Footer>
+ <SiteFooter topId={topId} />
<noscript>
- <div className={styles.noscript}>{noScript}</div>
+ <div className={styles.noscript}>{messages.noScript}</div>
</noscript>
</>
);
diff --git a/src/components/templates/layout/site-footer/index.ts b/src/components/templates/layout/site-footer/index.ts
new file mode 100644
index 0000000..cef0a6f
--- /dev/null
+++ b/src/components/templates/layout/site-footer/index.ts
@@ -0,0 +1 @@
+export * from './site-footer';
diff --git a/src/components/templates/layout/site-footer/site-footer.module.scss b/src/components/templates/layout/site-footer/site-footer.module.scss
new file mode 100644
index 0000000..935c163
--- /dev/null
+++ b/src/components/templates/layout/site-footer/site-footer.module.scss
@@ -0,0 +1,42 @@
+@use "../../../../styles/abstracts/functions" as fun;
+@use "../../../../styles/abstracts/mixins" as mix;
+
+.footer {
+ --navbar-size: #{fun.convert-px(80)};
+
+ display: flex;
+ flex-flow: column wrap;
+ gap: var(--spacing-xs);
+ place-items: center;
+ place-content: center;
+ padding: var(--spacing-md) 0 calc(var(--navbar-size) + var(--spacing-md));
+ border-top: fun.convert-px(3) solid var(--color-border-light);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ --navbar-size: 0px;
+
+ flex-flow: row wrap;
+ font-size: var(--font-size-sm);
+ }
+ }
+}
+
+.back-to-top {
+ position: fixed;
+ bottom: calc(var(--navbar-size, 0px) + var(--spacing-md));
+ right: var(--spacing-md);
+ transition: all 0.4s ease-in 0s;
+
+ &--hidden {
+ opacity: 0;
+ transform: translateY(calc(var(--button-height) + var(--spacing-md)));
+ visibility: hidden;
+ }
+
+ &--visible {
+ opacity: 1;
+ transform: translateY(0);
+ visibility: visible;
+ }
+}
diff --git a/src/components/templates/layout/site-footer/site-footer.test.tsx b/src/components/templates/layout/site-footer/site-footer.test.tsx
new file mode 100644
index 0000000..fa60b8f
--- /dev/null
+++ b/src/components/templates/layout/site-footer/site-footer.test.tsx
@@ -0,0 +1,20 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '../../../../../tests/utils';
+import { CONFIG } from '../../../../utils/config';
+import { ROUTES } from '../../../../utils/constants';
+import { SiteFooter } from './site-footer';
+
+describe('SiteFooter', () => {
+ it('renders the website colophon', () => {
+ render(<SiteFooter />);
+
+ expect(rtlScreen.getByRole('contentinfo')).toBeInTheDocument();
+ expect(rtlScreen.getByText(CONFIG.copyright.startYear)).toBeInTheDocument();
+ expect(rtlScreen.getByText(CONFIG.copyright.endYear)).toBeInTheDocument();
+ expect(rtlScreen.getByText(new RegExp(CONFIG.name))).toBeInTheDocument();
+ expect(rtlScreen.getByTitle('CC BY SA')).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('link', { name: 'Legal notice' })
+ ).toHaveAttribute('href', ROUTES.LEGAL_NOTICE);
+ });
+});
diff --git a/src/components/templates/layout/site-footer/site-footer.tsx b/src/components/templates/layout/site-footer/site-footer.tsx
new file mode 100644
index 0000000..b852b32
--- /dev/null
+++ b/src/components/templates/layout/site-footer/site-footer.tsx
@@ -0,0 +1,93 @@
+import { type ForwardRefRenderFunction, forwardRef } from 'react';
+import { useIntl } from 'react-intl';
+import { CONFIG } from '../../../../utils/config';
+import { ROUTES } from '../../../../utils/constants';
+import { useScrollPosition } from '../../../../utils/hooks';
+import { Footer, type FooterProps, Icon } from '../../../atoms';
+import {
+ BackToTop,
+ Colophon,
+ type ColophonLink,
+ Copyright,
+} from '../../../molecules';
+import styles from './site-footer.module.scss';
+
+export type SiteFooterProps = Omit<FooterProps, 'children'> & {
+ /**
+ * An id that will be use as anchor for the back to top button.
+ */
+ topId?: string;
+};
+
+const SiteFooterWithRef: ForwardRefRenderFunction<
+ HTMLElement,
+ SiteFooterProps
+> = ({ className = '', topId, ...props }, ref) => {
+ const footerClass = `${styles.footer} ${className}`;
+ const intl = useIntl();
+ const licenseName = intl.formatMessage({
+ defaultMessage: 'CC BY SA',
+ description: 'SiteFooter: the license name',
+ id: 'iTLvLX',
+ });
+ const backToTop = intl.formatMessage({
+ defaultMessage: 'Back to top',
+ description: 'SiteFooter: an accessible name for the back to top button',
+ id: 'OHvb01',
+ });
+ const footerNav: ColophonLink[] = [
+ {
+ id: 'legal-notice',
+ label: intl.formatMessage({
+ defaultMessage: 'Legal notice',
+ description: 'SiteFooter: Legal notice link label',
+ id: 'lsmD4c',
+ }),
+ href: ROUTES.LEGAL_NOTICE,
+ },
+ ];
+ const scrollPos = useScrollPosition();
+ const backToTopVisibilityBreakpoint = 300;
+ const backToTopClassName = [
+ styles['back-to-top'],
+ styles[
+ scrollPos.y > backToTopVisibilityBreakpoint
+ ? 'back-to-top--visible'
+ : 'back-to-top--hidden'
+ ],
+ ].join(' ');
+ const backToTopAnchor = topId ? `#${topId}` : undefined;
+
+ return (
+ <Footer {...props} className={footerClass} ref={ref}>
+ <Colophon
+ copyright={
+ <Copyright
+ from={CONFIG.copyright.startYear}
+ owner={CONFIG.name}
+ to={CONFIG.copyright.endYear}
+ />
+ }
+ license={
+ <Icon
+ heading={licenseName}
+ // eslint-disable-next-line react/jsx-no-literals
+ shape="cc-by-sa"
+ // eslint-disable-next-line react/jsx-no-literals
+ size="lg"
+ />
+ }
+ links={footerNav}
+ />
+ {backToTopAnchor ? (
+ <BackToTop
+ anchor={backToTopAnchor}
+ className={backToTopClassName}
+ label={backToTop}
+ />
+ ) : null}
+ </Footer>
+ );
+};
+
+export const SiteFooter = forwardRef(SiteFooterWithRef);
diff --git a/src/components/templates/layout/site-header/index.ts b/src/components/templates/layout/site-header/index.ts
new file mode 100644
index 0000000..7172be8
--- /dev/null
+++ b/src/components/templates/layout/site-header/index.ts
@@ -0,0 +1,2 @@
+export * from './site-header';
+export * from './site-navbar';
diff --git a/src/components/templates/layout/site-header/site-branding.test.tsx b/src/components/templates/layout/site-header/site-branding.test.tsx
new file mode 100644
index 0000000..db454e3
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-branding.test.tsx
@@ -0,0 +1,23 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '../../../../../tests/utils';
+import { SiteBranding } from './site-branding';
+import { CONFIG } from 'src/utils/config';
+import { ROUTES } from 'src/utils/constants';
+
+describe('SiteBranding', () => {
+ it('renders the website logo, name and baseline', () => {
+ render(<SiteBranding />);
+
+ expect(
+ rtlScreen.getByRole('img', { name: `${CONFIG.name} picture` })
+ ).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('img', { name: `${CONFIG.name} logo` })
+ ).toBeInTheDocument();
+ expect(rtlScreen.getByRole('link', { name: CONFIG.name })).toHaveAttribute(
+ 'href',
+ ROUTES.HOME
+ );
+ expect(rtlScreen.getByText(CONFIG.baseline)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/templates/layout/site-header/site-branding.tsx b/src/components/templates/layout/site-header/site-branding.tsx
new file mode 100644
index 0000000..f5a845d
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-branding.tsx
@@ -0,0 +1,91 @@
+import NextImage from 'next/image';
+import {
+ type CSSProperties,
+ forwardRef,
+ type ForwardRefRenderFunction,
+} from 'react';
+import { useIntl } from 'react-intl';
+import { CONFIG } from '../../../../utils/config';
+import { ROUTES } from '../../../../utils/constants';
+import { Heading, Logo } from '../../../atoms';
+import { Branding, FlippingLogo, type BrandingProps } from '../../../molecules';
+import styles from './site-header.module.scss';
+
+const brandingTitleStyles = {
+ '--typing-animation': 'blink 0.7s ease-in-out 0s 2, typing 4.3s linear 0s 1',
+} as CSSProperties;
+
+const brandingBaselineStyles = {
+ '--typing-animation':
+ 'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1',
+} as CSSProperties;
+
+export type SiteBrandingProps = Omit<
+ BrandingProps,
+ 'baseline' | 'logo' | 'name' | 'url'
+> & {
+ isHome?: boolean;
+};
+
+const SiteBrandingWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ SiteBrandingProps
+> = ({ isHome = false, ...props }, ref) => {
+ const intl = useIntl();
+ const photoAltText = intl.formatMessage(
+ {
+ defaultMessage: '{website} picture',
+ description: 'SiteBranding: photo alternative text',
+ id: 'dDwm38',
+ },
+ { website: CONFIG.name }
+ );
+ const logoTitle = intl.formatMessage(
+ {
+ defaultMessage: '{website} logo',
+ description: 'SiteBranding: logo title',
+ id: 'Vrw5/h',
+ },
+ { website: CONFIG.name }
+ );
+
+ return (
+ <Branding
+ {...props}
+ baseline={
+ <div className={styles.baseline} style={brandingBaselineStyles}>
+ {CONFIG.baseline}
+ </div>
+ }
+ logo={
+ <FlippingLogo
+ back={<Logo heading={logoTitle} />}
+ className={styles.logo}
+ front={
+ <NextImage
+ alt={photoAltText}
+ height={120}
+ // eslint-disable-next-line react/jsx-no-literals
+ src="/armand-philippot.jpg"
+ width={120}
+ />
+ }
+ />
+ }
+ name={
+ <Heading
+ className={styles.title}
+ isFake={!isHome}
+ level={1}
+ style={brandingTitleStyles}
+ >
+ {CONFIG.name}
+ </Heading>
+ }
+ ref={ref}
+ url={ROUTES.HOME}
+ />
+ );
+};
+
+export const SiteBranding = forwardRef(SiteBrandingWithRef);
diff --git a/src/components/templates/layout/site-header/site-header.module.scss b/src/components/templates/layout/site-header/site-header.module.scss
new file mode 100644
index 0000000..a48c054
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-header.module.scss
@@ -0,0 +1,90 @@
+@use "../../../../styles/abstracts/functions" as fun;
+@use "../../../../styles/abstracts/mixins" as mix;
+
+%typing-animation {
+ --typing-animation: none;
+
+ width: fit-content;
+ position: relative;
+ overflow: hidden;
+
+ &::after {
+ content: "|";
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ right: 0;
+ background: var(--color-bg);
+ color: var(--color-primary-darker);
+ font-weight: 400;
+ text-align: left;
+ visibility: hidden;
+ transform: translateX(100%);
+ transform-origin: right;
+ animation: var(--typing-animation);
+
+ :global {
+ animation: var(--typing-animation);
+ }
+ }
+}
+
+.header {
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-md) var(--spacing-xl);
+ align-items: center;
+ padding: clamp(var(--spacing-md), 3vh, var(--spacing-xl)) 0;
+ border-bottom: fun.convert-px(3) solid var(--color-border-light);
+}
+
+.branding,
+.navbar {
+ margin-inline: auto;
+}
+
+.logo {
+ --logo-size: #{clamp(
+ fun.convert-px(95),
+ calc(120px - 5vw),
+ fun.convert-px(120)
+ )};
+
+ animation: flip-logo 9s ease-in 0s 1;
+}
+
+.title {
+ font-size: var(--font-size-2xl);
+
+ @extend %typing-animation;
+}
+
+.baseline {
+ color: var(--color-fg-light);
+ font-size: var(--font-size-lg);
+ font-weight: 600;
+
+ @extend %typing-animation;
+}
+
+.search,
+.settings {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ min-width: 30ch;
+ }
+ }
+}
+
+@keyframes flip-logo {
+ 0%,
+ 90% {
+ transform: rotateY(180deg);
+ }
+
+ 100% {
+ transform: rotateY(0deg);
+ }
+}
diff --git a/src/components/templates/layout/site-header/site-header.test.tsx b/src/components/templates/layout/site-header/site-header.test.tsx
new file mode 100644
index 0000000..55ce072
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-header.test.tsx
@@ -0,0 +1,11 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '../../../../../tests/utils';
+import { SiteHeader } from './site-header';
+
+describe('SiteHeader', () => {
+ it('renders the website header', () => {
+ render(<SiteHeader />);
+
+ expect(rtlScreen.getByRole('banner')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/templates/layout/site-header/site-header.tsx b/src/components/templates/layout/site-header/site-header.tsx
new file mode 100644
index 0000000..3e06350
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-header.tsx
@@ -0,0 +1,25 @@
+import { type ForwardRefRenderFunction, forwardRef } from 'react';
+import { Header, type HeaderProps } from '../../../atoms';
+import { SiteBranding } from './site-branding';
+import styles from './site-header.module.scss';
+import { SiteNavbar } from './site-navbar';
+
+export type SiteHeaderProps = Omit<HeaderProps, 'children'> & {
+ isHome?: boolean;
+};
+
+const SiteHeaderWithRef: ForwardRefRenderFunction<
+ HTMLElement,
+ SiteHeaderProps
+> = ({ className = '', isHome = false, ...props }, ref) => {
+ const headerClass = `${styles.header} ${className}`;
+
+ return (
+ <Header {...props} className={headerClass} ref={ref}>
+ <SiteBranding className={styles.branding} isHome={isHome} />
+ <SiteNavbar className={styles.navbar} />
+ </Header>
+ );
+};
+
+export const SiteHeader = forwardRef(SiteHeaderWithRef);
diff --git a/src/components/templates/layout/site-header/site-navbar.test.tsx b/src/components/templates/layout/site-header/site-navbar.test.tsx
new file mode 100644
index 0000000..cf40927
--- /dev/null
+++ b/src/components/templates/layout/site-header/site-navbar.test.tsx
@@ -0,0 +1,73 @@
+import { describe, expect, it } from '@jest/globals';
+import { userEvent } from '@testing-library/user-event';
+import mockRouter from 'next-router-mock';
+import { MemoryRouterProvider } from 'next-router-mock/MemoryRouterProvider';
+import {
+ render,
+ screen as rtlScreen,
+ waitFor,
+} from '../../../../../tests/utils';
+import { SiteNavbar } from './site-navbar';
+import { ROUTES } from 'src/utils/constants';
+
+describe('SiteNavbar', () => {
+ it('renders the main nav, a search form and a settings form', () => {
+ render(<SiteNavbar />);
+
+ expect(
+ rtlScreen.getByRole('checkbox', { name: 'Open menu' })
+ ).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('checkbox', { name: 'Open search' })
+ ).toBeInTheDocument();
+ expect(
+ rtlScreen.getByRole('checkbox', { name: 'Open settings' })
+ ).toBeInTheDocument();
+ });
+
+ it('can give focus to the search input on activation', async () => {
+ const user = userEvent.setup();
+
+ render(<SiteNavbar />);
+
+ /* It seems we cannot use it with waitFor... the assertions count is
+ * inaccurate. */
+ // expect.assertions(1);
+
+ await user.click(rtlScreen.getByRole('checkbox', { name: 'Open search' }));
+
+ await waitFor(() => {
+ expect(rtlScreen.getByRole('searchbox')).toHaveFocus();
+ });
+ });
+
+ it('can submit the search form', async () => {
+ const user = userEvent.setup();
+ const keywords = 'keywords';
+
+ render(
+ <MemoryRouterProvider>
+ <SiteNavbar />
+ </MemoryRouterProvider>
+ );
+
+ await user.click(rtlScreen.getByRole('checkbox', { name: 'Open search' }));
+ await user.type(rtlScreen.getByRole('searchbox'), keywords);
+ await user.click(rtlScreen.getByRole('button', { name: 'Search' }));
+
+ expect(mockRouter.asPath).toBe(`${ROUTES.SEARCH}?s=${keywords}`);
+ });
+
+ it('does not submit the search form without keywords', async () => {
+ const user = userEvent.setup();
+
+ render(<SiteNavbar />);
+
+ await user.click(rtlScreen.getByRole('checkbox', { name: 'Open search' }));
+ await user.click(rtlScreen.getByRole('button', { name: 'Search' }));
+
+ expect(
+ rtlScreen.getByText(/Query must be longer than one character./)
+ ).toBeInTheDocument();
+ });
+});
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);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7b59fdb..aac327d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -71,6 +71,10 @@
"defaultMessage": "Projects",
"description": "Breadcrumb: projects label"
},
+ "2By3AZ": {
+ "defaultMessage": "Open menu",
+ "description": "SiteNavbar: main nav button label in navbar"
+ },
"2D9tB5": {
"defaultMessage": "Topics",
"description": "BlogPage: topics list widget title"
@@ -107,14 +111,18 @@
"defaultMessage": "Loading the requested article...",
"description": "ArticlePage: loading article message"
},
- "52H2HA": {
- "defaultMessage": "{website} logo",
- "description": "Layout: logo title"
+ "5C+1PP": {
+ "defaultMessage": "Blog",
+ "description": "SiteNavbar: main nav - blog link"
},
"5eD6y2": {
"defaultMessage": "Full",
"description": "AckeeToggle: full option name"
},
+ "5eq0+c": {
+ "defaultMessage": "Search",
+ "description": "SiteNavbar: search modal title in navbar"
+ },
"6GySNl": {
"defaultMessage": "Copy",
"description": "usePrism: copy button text (not clicked)"
@@ -143,10 +151,6 @@
"defaultMessage": "Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.",
"description": "AckeeToggle: tooltip message"
},
- "8jjY1X": {
- "defaultMessage": "{website} picture",
- "description": "Layout: photo alternative text"
- },
"8q5PXx": {
"defaultMessage": "{date} at {time}",
"description": "Time: readable date and time"
@@ -175,10 +179,6 @@
"defaultMessage": "Technologies:",
"description": "Meta: technologies label"
},
- "AE4kCD": {
- "defaultMessage": "Contact",
- "description": "Layout: main nav - contact link"
- },
"AN9iy7": {
"defaultMessage": "Contact",
"description": "ContactPage: page title"
@@ -215,18 +215,10 @@
"defaultMessage": "Failed to load.",
"description": "BlogPage: failed to load text"
},
- "C2YcUJ": {
- "defaultMessage": "Query must be longer than one character.",
- "description": "Layout: invalid query message"
- },
"C6oK7h": {
"defaultMessage": "Query must be longer than one character.",
"description": "404Page: invalid query message"
},
- "D8vB38": {
- "defaultMessage": "Blog",
- "description": "Layout: main nav - blog link"
- },
"Dq6+WH": {
"defaultMessage": "Thematics",
"description": "SearchPage: thematics list widget title"
@@ -247,10 +239,6 @@
"defaultMessage": "{title} cover",
"description": "ProjectsPage: figure (cover) accessible name"
},
- "Fgt/RZ": {
- "defaultMessage": "Open menu",
- "description": "Layout: main nav button label in navbar"
- },
"GVpTIl": {
"defaultMessage": "Topics",
"description": "Error404Page: topics list widget title"
@@ -291,6 +279,10 @@
"defaultMessage": "Current page, page {number}",
"description": "BlogPage: current page label"
},
+ "JXLaT8": {
+ "defaultMessage": "Projects",
+ "description": "SiteNavbar: main nav - projects link"
+ },
"JbT+fA": {
"defaultMessage": "Updated on:",
"description": "ProjectOverview: update date label"
@@ -311,10 +303,6 @@
"defaultMessage": "Other thematics",
"description": "ThematicPage: other thematics list widget title"
},
- "Kjj1Zk": {
- "defaultMessage": "Back to top",
- "description": "Layout: an accessible name for the back to top button"
- },
"KnWeKh": {
"defaultMessage": "Page not found",
"description": "Error404Page: page title"
@@ -323,9 +311,9 @@
"defaultMessage": "All posts in {thematicName}",
"description": "ThematicPage: posts list heading"
},
- "Mq+O6q": {
- "defaultMessage": "Search",
- "description": "Layout: search modal title in navbar"
+ "MJLr6U": {
+ "defaultMessage": "CV",
+ "description": "SiteNavbar: main nav - cv link"
},
"N44SOc": {
"defaultMessage": "Projects",
@@ -379,6 +367,10 @@
"defaultMessage": "{websiteName} | Front-end developer: WordPress/React",
"description": "HomePage: SEO - Page title"
},
+ "PnrHgZ": {
+ "defaultMessage": "Home",
+ "description": "SiteNavbar: main nav - home link"
+ },
"Q3oEQn": {
"defaultMessage": "LinkedIn profile",
"description": "ContactPage: LinkedIn profile link"
@@ -387,6 +379,10 @@
"defaultMessage": "Dark Theme 🌙",
"description": "usePrism: toggle dark theme button text"
},
+ "QQAcaS": {
+ "defaultMessage": "Main navigation",
+ "description": "SiteNavbar: main nav accessible name"
+ },
"Qa9twM": {
"defaultMessage": "Reply",
"description": "CommentsList: reply button"
@@ -403,10 +399,6 @@
"defaultMessage": "License:",
"description": "ProjectOverview: license label"
},
- "R895yC": {
- "defaultMessage": "CV",
- "description": "Layout: main nav - cv link"
- },
"RwI3B9": {
"defaultMessage": "Loading the repository popularity...",
"description": "ProjectsPage: loading repository popularity"
@@ -435,6 +427,10 @@
"defaultMessage": "Query must be longer than one character.",
"description": "NoResults: invalid query message"
},
+ "Vrw5/h": {
+ "defaultMessage": "{website} logo",
+ "description": "SiteBranding: logo title"
+ },
"WDwNDl": {
"defaultMessage": "Search",
"description": "SearchPage: SEO - Page title"
@@ -451,14 +447,14 @@
"defaultMessage": "Search for:",
"description": "SearchForm: field accessible label"
},
+ "XGmQXV": {
+ "defaultMessage": "Contact",
+ "description": "SiteNavbar: main nav - contact link"
+ },
"XKy7rx": {
"defaultMessage": "You can also try a search:",
"description": "Error404Page: try a search message"
},
- "XRwEoA": {
- "defaultMessage": "Open search",
- "description": "Layout: search button label in navbar"
- },
"Y7XdNp": {
"defaultMessage": "Leave a comment",
"description": "PageComments: the section title of the comment form"
@@ -471,6 +467,10 @@
"defaultMessage": "Light theme",
"description": "ThemeToggle: light theme label"
},
+ "Z/rsgm": {
+ "defaultMessage": "Open search",
+ "description": "SiteNavbar: search button label in navbar"
+ },
"ZB/Aw2": {
"defaultMessage": "Partial includes only page url, views and duration.",
"description": "AckeeToggle: tooltip message"
@@ -507,10 +507,6 @@
"defaultMessage": "Contact form",
"description": "Contact: form accessible name"
},
- "bojYF5": {
- "defaultMessage": "Home",
- "description": "Layout: main nav - home link"
- },
"c0Oecl": {
"defaultMessage": "Created on:",
"description": "ProjectOverview: creation date label"
@@ -523,9 +519,9 @@
"defaultMessage": "Popularity:",
"description": "ProjectOverview: popularity label"
},
- "dfTljv": {
- "defaultMessage": "Main navigation",
- "description": "Layout: main nav accessible name"
+ "dDwm38": {
+ "defaultMessage": "{website} picture",
+ "description": "SiteBranding: photo alternative text"
},
"eys2uX": {
"defaultMessage": "Table of Contents",
@@ -547,10 +543,6 @@
"defaultMessage": "Code blocks:",
"description": "PrismThemeToggle: theme label"
},
- "h3J0a+": {
- "defaultMessage": "Settings form",
- "description": "Layout: an accessible name for the settings form in navbar"
- },
"hGvQpI": {
"defaultMessage": "Load more posts?",
"description": "PostsList: load more button"
@@ -583,6 +575,10 @@
"defaultMessage": "Reading time:",
"description": "PageHeader: reading time label"
},
+ "l50cYa": {
+ "defaultMessage": "Open settings",
+ "description": "SiteNavbar: settings button label in navbar"
+ },
"lKhTGM": {
"defaultMessage": "Use Ctrl+c to copy",
"description": "usePrism: copy button error text"
@@ -591,14 +587,14 @@
"defaultMessage": "Legal notice",
"description": "SiteFooter: Legal notice link label"
},
- "mDKiaN": {
- "defaultMessage": "Open settings",
- "description": "Layout: settings button label in navbar"
- },
"nGss/j": {
"defaultMessage": "Ackee tracking (analytics)",
"description": "AckeeToggle: tooltip title"
},
+ "nRzO0T": {
+ "defaultMessage": "Query must be longer than one character.",
+ "description": "SiteNavbar: invalid query message"
+ },
"ndAawq": {
"defaultMessage": "Leave a reply to comment {id}",
"description": "ReplyCommentForm: an accessible name for the reply form"
@@ -611,18 +607,10 @@
"defaultMessage": "Copied!",
"description": "usePrism: copy button text (clicked)"
},
- "nwbzKm": {
- "defaultMessage": "Legal notice",
- "description": "Layout: Legal notice label"
- },
"o+wCJz": {
"defaultMessage": "Comment form",
"description": "PageComments: an accessible name for the comment form"
},
- "o3WSz5": {
- "defaultMessage": "Settings",
- "description": "Layout: settings modal title in navbar"
- },
"ofQPC+": {
"defaultMessage": "Share on LinkedIn",
"description": "SharingWidget: LinkedIn sharing link"
@@ -651,10 +639,6 @@
"defaultMessage": "Discover search results for {query} on {websiteName}.",
"description": "SearchPage: SEO - Meta description"
},
- "qnwsWV": {
- "defaultMessage": "Projects",
- "description": "Layout: main nav - projects link"
- },
"s8/tyz": {
"defaultMessage": "Object:",
"description": "ContactForm: object label"
@@ -691,6 +675,10 @@
"defaultMessage": "Website:",
"description": "CommentForm: website label"
},
+ "uKef8u": {
+ "defaultMessage": "Settings",
+ "description": "SiteNavbar: settings modal title in navbar"
+ },
"uZj4QI": {
"defaultMessage": "Cancel reply",
"description": "CommentsList: cancel reply button"
@@ -727,10 +715,6 @@
"defaultMessage": "{minutesCount, plural, =0 {Less than one minute} one {# minute} other {# minutes}}",
"description": "PostPreviewMeta: rounded minutes count"
},
- "yB1SPF": {
- "defaultMessage": "CC BY SA",
- "description": "Layout: copyright title"
- },
"yN5P+m": {
"defaultMessage": "Message:",
"description": "ContactForm: message label"
@@ -742,5 +726,9 @@
"zbzlb1": {
"defaultMessage": "Page {number}",
"description": "BlogPage: page number"
+ },
+ "zhjPcZ": {
+ "defaultMessage": "Settings form",
+ "description": "SiteNavbar: an accessible name for the settings form in navbar"
}
}
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 1ca2efd..17514a3 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -71,6 +71,10 @@
"defaultMessage": "Projets",
"description": "Breadcrumb: projects label"
},
+ "2By3AZ": {
+ "defaultMessage": "Ouvrir le menu",
+ "description": "SiteNavbar: main nav button label in navbar"
+ },
"2D9tB5": {
"defaultMessage": "Sujets",
"description": "BlogPage: topics list widget title"
@@ -107,14 +111,18 @@
"defaultMessage": "Chargement de l’article demandé…",
"description": "ArticlePage: loading article message"
},
- "52H2HA": {
- "defaultMessage": "Logo du site d’{website}",
- "description": "Layout: logo title"
+ "5C+1PP": {
+ "defaultMessage": "Blog",
+ "description": "SiteNavbar: main nav - blog link"
},
"5eD6y2": {
"defaultMessage": "Complet",
"description": "AckeeToggle: full option name"
},
+ "5eq0+c": {
+ "defaultMessage": "Recherche",
+ "description": "SiteNavbar: search modal title in navbar"
+ },
"6GySNl": {
"defaultMessage": "Copier",
"description": "usePrism: copy button text (not clicked)"
@@ -143,10 +151,6 @@
"defaultMessage": "Complet inclut toutes les informations de Partiel ainsi que des informations à propos du site référent, du système d’exploitation, de l’appareil, du navigateur, de la taille d’écran et de la langue.",
"description": "AckeeToggle: tooltip message"
},
- "8jjY1X": {
- "defaultMessage": "Photo d’{website}",
- "description": "Layout: photo alternative text"
- },
"8q5PXx": {
"defaultMessage": "{date} Ă  {time}",
"description": "Time: readable date and time"
@@ -175,10 +179,6 @@
"defaultMessage": "Technologies :",
"description": "Meta: technologies label"
},
- "AE4kCD": {
- "defaultMessage": "Contact",
- "description": "Layout: main nav - contact link"
- },
"AN9iy7": {
"defaultMessage": "Contact",
"description": "ContactPage: page title"
@@ -215,18 +215,10 @@
"defaultMessage": "Échec du chargement.",
"description": "BlogPage: failed to load text"
},
- "C2YcUJ": {
- "defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.",
- "description": "Layout: invalid query message"
- },
"C6oK7h": {
"defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.",
"description": "404Page: invalid query message"
},
- "D8vB38": {
- "defaultMessage": "Blog",
- "description": "Layout: main nav - blog link"
- },
"Dq6+WH": {
"defaultMessage": "Thématiques",
"description": "SearchPage: thematics list widget title"
@@ -247,10 +239,6 @@
"defaultMessage": "Illustration de {title}",
"description": "ProjectsPage: figure (cover) accessible name"
},
- "Fgt/RZ": {
- "defaultMessage": "Ouvrir le menu",
- "description": "Layout: main nav button label in navbar"
- },
"GVpTIl": {
"defaultMessage": "Sujets",
"description": "Error404Page: topics list widget title"
@@ -291,6 +279,10 @@
"defaultMessage": "Page actuelle, page {number}",
"description": "BlogPage: current page label"
},
+ "JXLaT8": {
+ "defaultMessage": "Projets",
+ "description": "SiteNavbar: main nav - projects link"
+ },
"JbT+fA": {
"defaultMessage": "Mis à jour le :",
"description": "ProjectOverview: update date label"
@@ -311,10 +303,6 @@
"defaultMessage": "Autres thématiques",
"description": "ThematicPage: other thematics list widget title"
},
- "Kjj1Zk": {
- "defaultMessage": "Retour en haut de page",
- "description": "Layout: an accessible name for the back to top button"
- },
"KnWeKh": {
"defaultMessage": "Page non trouvée",
"description": "Error404Page: page title"
@@ -323,9 +311,9 @@
"defaultMessage": "Tous les articles dans {thematicName}",
"description": "ThematicPage: posts list heading"
},
- "Mq+O6q": {
- "defaultMessage": "Recherche",
- "description": "Layout: search modal title in navbar"
+ "MJLr6U": {
+ "defaultMessage": "CV",
+ "description": "SiteNavbar: main nav - cv link"
},
"N44SOc": {
"defaultMessage": "Projets",
@@ -355,6 +343,10 @@
"defaultMessage": "{thematicsCount, plural, =0 {Thématiques :} one {Thématique :} other {Thématiques :}}",
"description": "PageHeader: thematics label"
},
+ "OHvb01": {
+ "defaultMessage": "Retour en haut de page",
+ "description": "SiteFooter: an accessible name for the back to top button"
+ },
"OL0Yzx": {
"defaultMessage": "Publier",
"description": "CommentForm: submit button"
@@ -375,6 +367,10 @@
"defaultMessage": "{websiteName} | Intégrateur web - Développeur WordPress / React",
"description": "HomePage: SEO - Page title"
},
+ "PnrHgZ": {
+ "defaultMessage": "Accueil",
+ "description": "SiteNavbar: main nav - home link"
+ },
"Q3oEQn": {
"defaultMessage": "Profil LinkedIn",
"description": "ContactPage: LinkedIn profile link"
@@ -383,6 +379,10 @@
"defaultMessage": "Thème sombre 🌙",
"description": "usePrism: toggle dark theme button text"
},
+ "QQAcaS": {
+ "defaultMessage": "Navigation principale",
+ "description": "SiteNavbar: main nav accessible name"
+ },
"Qa9twM": {
"defaultMessage": "Répondre",
"description": "CommentsList: reply button"
@@ -399,10 +399,6 @@
"defaultMessage": "Licence :",
"description": "ProjectOverview: license label"
},
- "R895yC": {
- "defaultMessage": "CV",
- "description": "Layout: main nav - cv link"
- },
"RwI3B9": {
"defaultMessage": "Chargement de la popularité du dépôt…",
"description": "ProjectsPage: loading repository popularity"
@@ -431,6 +427,10 @@
"defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.",
"description": "NoResults: invalid query message"
},
+ "Vrw5/h": {
+ "defaultMessage": "Logo d’{website}",
+ "description": "SiteBranding: logo title"
+ },
"WDwNDl": {
"defaultMessage": "Recherche",
"description": "SearchPage: SEO - Page title"
@@ -447,14 +447,14 @@
"defaultMessage": "Rechercher :",
"description": "SearchForm: field accessible label"
},
+ "XGmQXV": {
+ "defaultMessage": "Contact",
+ "description": "SiteNavbar: main nav - contact link"
+ },
"XKy7rx": {
"defaultMessage": "Vous pouvez également tenter une recherche :",
"description": "Error404Page: try a search message"
},
- "XRwEoA": {
- "defaultMessage": "Ouvrir la recherche",
- "description": "Layout: search button label in navbar"
- },
"Y7XdNp": {
"defaultMessage": "Laisser un commentaire",
"description": "PageComments: the section title of the comment form"
@@ -467,6 +467,10 @@
"defaultMessage": "Thème clair",
"description": "ThemeToggle: light theme label"
},
+ "Z/rsgm": {
+ "defaultMessage": "Ouvrir le formulaire de recherche",
+ "description": "SiteNavbar: search button label in navbar"
+ },
"ZB/Aw2": {
"defaultMessage": "Partiel inclut seulement l’url de la page, le nombre de visites et la durée.",
"description": "AckeeToggle: tooltip message"
@@ -503,10 +507,6 @@
"defaultMessage": "Formulaire de contact",
"description": "Contact: form accessible name"
},
- "bojYF5": {
- "defaultMessage": "Accueil",
- "description": "Layout: main nav - home link"
- },
"c0Oecl": {
"defaultMessage": "Créé le :",
"description": "ProjectOverview: creation date label"
@@ -519,9 +519,9 @@
"defaultMessage": "Popularité :",
"description": "ProjectOverview: popularity label"
},
- "dfTljv": {
- "defaultMessage": "Navigation principale",
- "description": "Layout: main nav accessible name"
+ "dDwm38": {
+ "defaultMessage": "Photo d’{website}",
+ "description": "SiteBranding: photo alternative text"
},
"eys2uX": {
"defaultMessage": "Table des matières",
@@ -543,10 +543,6 @@
"defaultMessage": "Blocs de code :",
"description": "PrismThemeToggle: theme label"
},
- "h3J0a+": {
- "defaultMessage": "Formulaire des réglages",
- "description": "Layout: an accessible name for the settings form in navbar"
- },
"hGvQpI": {
"defaultMessage": "Charger plus d’articles ?",
"description": "PostsList: load more button"
@@ -563,6 +559,10 @@
"defaultMessage": "Illustration de {postTitle}",
"description": "PostPreview: an accessible name for the figure wrapping the cover"
},
+ "iTLvLX": {
+ "defaultMessage": "CC BY SA",
+ "description": "SiteFooter: the license name"
+ },
"j5k9Fe": {
"defaultMessage": "Accueil",
"description": "Breadcrumb: home label"
@@ -575,18 +575,26 @@
"defaultMessage": "Temps de lecture :",
"description": "PageHeader: reading time label"
},
+ "l50cYa": {
+ "defaultMessage": "Ouvrir les réglages",
+ "description": "SiteNavbar: settings button label in navbar"
+ },
"lKhTGM": {
"defaultMessage": "Utilisez Ctrl+c pour copier",
"description": "usePrism: copy button error text"
},
- "mDKiaN": {
- "defaultMessage": "Ouvrir les réglages",
- "description": "Layout: settings button label in navbar"
+ "lsmD4c": {
+ "defaultMessage": "Mentions légales",
+ "description": "SiteFooter: Legal notice link label"
},
"nGss/j": {
"defaultMessage": "Suivi Ackee (analytique)",
"description": "AckeeToggle: tooltip title"
},
+ "nRzO0T": {
+ "defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.",
+ "description": "SiteNavbar: invalid query message"
+ },
"ndAawq": {
"defaultMessage": "Répondre au commentaire {id}",
"description": "ReplyCommentForm: an accessible name for the reply form"
@@ -599,18 +607,10 @@
"defaultMessage": "Copié !",
"description": "usePrism: copy button text (clicked)"
},
- "nwbzKm": {
- "defaultMessage": "Mentions légales",
- "description": "Layout: Legal notice label"
- },
"o+wCJz": {
"defaultMessage": "Formulaire des commentaires",
"description": "PageComments: an accessible name for the comment form"
},
- "o3WSz5": {
- "defaultMessage": "Réglages",
- "description": "Layout: settings modal title in navbar"
- },
"ofQPC+": {
"defaultMessage": "Partager sur LinkedIn",
"description": "SharingWidget: LinkedIn sharing link"
@@ -639,10 +639,6 @@
"defaultMessage": "Découvrez les résultats de recherche pour {query} sur {websiteName}.",
"description": "SearchPage: SEO - Meta description"
},
- "qnwsWV": {
- "defaultMessage": "Projets",
- "description": "Layout: main nav - projects link"
- },
"s8/tyz": {
"defaultMessage": "Sujet :",
"description": "ContactForm: object label"
@@ -679,6 +675,10 @@
"defaultMessage": "Site web :",
"description": "CommentForm: website label"
},
+ "uKef8u": {
+ "defaultMessage": "Réglages",
+ "description": "SiteNavbar: settings modal title in navbar"
+ },
"uZj4QI": {
"defaultMessage": "Annuler la réponse",
"description": "CommentsList: cancel reply button"
@@ -715,10 +715,6 @@
"defaultMessage": "{minutesCount, plural, =0 {Moins d’une minute} one {# minute} other {# minutes}}",
"description": "PostPreviewMeta: rounded minutes count"
},
- "yB1SPF": {
- "defaultMessage": "CC BY SA",
- "description": "Layout: copyright title"
- },
"yN5P+m": {
"defaultMessage": "Message :",
"description": "ContactForm: message label"
@@ -730,5 +726,9 @@
"zbzlb1": {
"defaultMessage": "Page {number}",
"description": "BlogPage: page number"
+ },
+ "zhjPcZ": {
+ "defaultMessage": "Formulaire des réglages",
+ "description": "SiteNavbar: an accessible name for the settings form in navbar"
}
}
diff --git a/src/styles/base/_base.scss b/src/styles/base/_base.scss
index 91989bd..1b52515 100644
--- a/src/styles/base/_base.scss
+++ b/src/styles/base/_base.scss
@@ -38,6 +38,59 @@ p + iframe {
margin-top: 0;
}
+p {
+ font-size: var(--font-size-md);
+ margin: 0 0 var(--spacing-sm);
+}
+
+small {
+ font-size: var(--font-size-sm);
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ line-height: var(--line-height);
+}
+
+code,
+kbd,
+pre,
+var,
+samp {
+ font-family: var(--font-family-mono);
+}
+
+pre {
+ display: block;
+ max-width: 100%;
+ overflow: auto;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ word-wrap: normal;
+}
+
+:not(pre) > code,
+kbd,
+var,
+samp {
+ background: var(--color-bg-code);
+ border: fun.convert-px(1) solid var(--color-border);
+ border-radius: fun.convert-px(3);
+ color: var(--color-primary-darker);
+ font-style: normal;
+ padding: fun.convert-px(2) fun.convert-px(5) fun.convert-px(1)
+ fun.convert-px(5);
+}
+
+kbd {
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 fun.convert-px(1)
+ var(--color-shadow);
+}
+
* {
scrollbar-color: var(--color-primary) var(--color-bg-tertiary);
scrollbar-width: auto;
diff --git a/src/styles/base/_spacings.scss b/src/styles/base/_spacings.scss
index 3cff009..13a1fbe 100644
--- a/src/styles/base/_spacings.scss
+++ b/src/styles/base/_spacings.scss
@@ -24,5 +24,4 @@
--spacing-xl: clamp(#{var.spacing("lg")}, 1ex + 4vw, #{var.spacing("xl")});
--spacing-2xl: clamp(#{var.spacing("xl")}, 1ex + 5vw, #{var.spacing("2xl")});
--spacing-3xl: clamp(#{var.spacing("2xl")}, 1ex + 6vw, #{var.spacing("3xl")});
- --toolbar-size: #{fun.convert-px(80)};
}
diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss
deleted file mode 100644
index 170f246..0000000
--- a/src/styles/base/_typography.scss
+++ /dev/null
@@ -1,53 +0,0 @@
-@use "../abstracts/functions" as fun;
-
-p {
- font-size: var(--font-size-md);
- margin: 0 0 var(--spacing-sm);
-}
-
-small {
- font-size: var(--font-size-sm);
-}
-
-button,
-input,
-optgroup,
-select,
-textarea {
- line-height: var(--line-height);
-}
-
-code,
-kbd,
-pre,
-var {
- font-family: var(--font-family-mono);
-}
-
-:not(pre) > code,
-kbd,
-var,
-samp {
- background: var(--color-bg-code);
- border: fun.convert-px(1) solid var(--color-border);
- border-radius: fun.convert-px(3);
- color: var(--color-primary-darker);
- font-style: normal;
- padding: fun.convert-px(2) fun.convert-px(5) fun.convert-px(1)
- fun.convert-px(5);
-}
-
-kbd {
- box-shadow: fun.convert-px(1) fun.convert-px(1) 0 fun.convert-px(1)
- var(--color-shadow);
-}
-
-pre {
- display: block;
- max-width: 100%;
- overflow: auto;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
-}
diff --git a/src/styles/globals.scss b/src/styles/globals.scss
index 8cf7296..32f325f 100644
--- a/src/styles/globals.scss
+++ b/src/styles/globals.scss
@@ -19,7 +19,6 @@
@use "base/helpers";
@use "base/icons";
@use "base/spacings";
-@use "base/typography";
/**
* 3.0. Themes
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 7129624..26cbeaa 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -15,6 +15,7 @@ export const ROUTES = {
BLOG: '/blog',
CONTACT: '/contact',
CV: '/cv',
+ HOME: '/',
LEGAL_NOTICE: '/mentions-legales',
NOT_FOUND: '/404',
PROJECTS: '/projets',