summaryrefslogtreecommitdiffstats
path: root/src/components/templates/layout/layout.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-24 19:35:12 +0200
committerGitHub <noreply@github.com>2022-05-24 19:35:12 +0200
commitc85ab5ad43ccf52881ee224672c41ec30021cf48 (patch)
tree8058808d9bfca19383f120c46b34d99ff2f89f63 /src/components/templates/layout/layout.tsx
parent52404177c07a2aab7fc894362fb3060dff2431a0 (diff)
parent11b9de44a4b2f305a6a484187805e429b2767118 (diff)
refactor: use storybook and atomic design (#16)
BREAKING CHANGE: rewrite most of the Typescript types, so the content format (the meta in particular) needs to be updated.
Diffstat (limited to 'src/components/templates/layout/layout.tsx')
-rw-r--r--src/components/templates/layout/layout.tsx242
1 files changed, 242 insertions, 0 deletions
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
new file mode 100644
index 0000000..8f0d4e7
--- /dev/null
+++ b/src/components/templates/layout/layout.tsx
@@ -0,0 +1,242 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Career from '@components/atoms/icons/career';
+import CCBySA from '@components/atoms/icons/cc-by-sa';
+import ComputerScreen from '@components/atoms/icons/computer-screen';
+import Envelop from '@components/atoms/icons/envelop';
+import Home from '@components/atoms/icons/home';
+import PostsStack from '@components/atoms/icons/posts-stack';
+import Main from '@components/atoms/layout/main';
+import NoScript from '@components/atoms/layout/no-script';
+import Footer, { type FooterProps } from '@components/organisms/layout/footer';
+import Header, { type HeaderProps } from '@components/organisms/layout/header';
+import { type NextPageWithLayoutOptions } from '@ts/types/app';
+import useScrollPosition from '@utils/hooks/use-scroll-position';
+import useSettings from '@utils/hooks/use-settings';
+import Script from 'next/script';
+import { FC, ReactElement, ReactNode, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
+import styles from './layout.module.scss';
+
+export type QueryAction = SearchAction & {
+ 'query-input': string;
+};
+
+export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
+ /**
+ * The layout main content.
+ */
+ children: ReactNode;
+ /**
+ * Determine if article has a comments section.
+ */
+ withExtraPadding?: boolean;
+ /**
+ * Determine if article should use grid. Default: false.
+ */
+ useGrid?: boolean;
+};
+
+/**
+ * Layout component
+ *
+ * Render the base layout used by all pages.
+ */
+const Layout: FC<LayoutProps> = ({
+ children,
+ withExtraPadding = false,
+ isHome,
+ useGrid = false,
+}) => {
+ const intl = useIntl();
+ const { website } = useSettings();
+ const { baseline, copyright, locales, name, picture, url } = website;
+ const articleGridClass = useGrid ? 'article--grid' : '';
+ const articleCommentsClass = withExtraPadding ? 'article--padding' : '';
+
+ 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 copyrightData = {
+ dates: {
+ start: copyright.start,
+ end: copyright.end,
+ },
+ owner: name,
+ icon: <CCBySA />,
+ };
+
+ 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 mainNav: HeaderProps['nav'] = [
+ { id: 'home', label: homeLabel, href: '/', logo: <Home /> },
+ { id: 'blog', label: blogLabel, href: '/blog', logo: <PostsStack /> },
+ {
+ id: 'projects',
+ label: projectsLabel,
+ href: '/projets',
+ logo: <ComputerScreen />,
+ },
+ { id: 'cv', label: cvLabel, href: '/cv', logo: <Career /> },
+ { id: 'contact', label: contactLabel, href: '/contact', logo: <Envelop /> },
+ ];
+
+ const legalNoticeLabel = intl.formatMessage({
+ defaultMessage: 'Legal notice',
+ description: 'Layout: Legal notice label',
+ id: 'nwbzKm',
+ });
+
+ const footerNav: FooterProps['navItems'] = [
+ { id: 'legal-notice', label: legalNoticeLabel, href: '/mentions-legales' },
+ ];
+
+ const searchActionSchema: QueryAction = {
+ '@type': 'SearchAction',
+ target: {
+ '@type': 'EntryPoint',
+ urlTemplate: `${url}/recherche?s={search_term_string}`,
+ },
+ query: 'required',
+ 'query-input': 'required name=search_term_string',
+ };
+
+ const schemaJsonLd: WithContext<WebSite> = {
+ '@context': 'https://schema.org',
+ '@id': `${url}`,
+ '@type': 'WebSite',
+ name: name,
+ description: baseline,
+ url: url,
+ author: { '@id': `${url}/#branding` },
+ copyrightYear: Number(copyright.start),
+ creator: { '@id': `${url}/#branding` },
+ editor: { '@id': `${url}/#branding` },
+ inLanguage: locales.default,
+ potentialAction: searchActionSchema,
+ };
+
+ const brandingSchema: WithContext<Person> = {
+ '@context': 'https://schema.org',
+ '@type': 'Person',
+ '@id': `${url}/#branding`,
+ name: name,
+ url: url,
+ jobTitle: baseline,
+ image: picture.src,
+ subjectOf: { '@id': `${url}` },
+ };
+
+ const [backToTopClassName, setBackToTopClassName] = useState<string>(
+ styles['back-to-top--hidden']
+ );
+ const updateBackToTopClassName = () => {
+ setBackToTopClassName(
+ window.scrollY > 300
+ ? styles['back-to-top--visible']
+ : styles['back-to-top--hidden']
+ );
+ };
+
+ useScrollPosition(updateBackToTopClassName);
+
+ return (
+ <>
+ <Script
+ id="schema-layout"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <Script
+ id="schema-branding"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }}
+ />
+ <noscript>
+ <div className={styles['noscript-spacing']}></div>
+ </noscript>
+ <span tabIndex={-1}></span>
+ <ButtonLink target="#main" className="screen-reader-text">
+ {skipToContent}
+ </ButtonLink>
+ <Header
+ ackeeStorageKey="ackee-tracking"
+ baseline={baseline}
+ className={styles.header}
+ isHome={isHome}
+ motionStorageKey="reduced-motion"
+ nav={mainNav}
+ photo={picture}
+ searchPage="/recherche"
+ title={name}
+ withLink={true}
+ />
+ <Main id="main" className={styles.main}>
+ <article
+ className={`${styles[articleGridClass]} ${styles[articleCommentsClass]}`}
+ >
+ {children}
+ </article>
+ </Main>
+ <Footer
+ copyright={copyrightData}
+ navItems={footerNav}
+ topId="top"
+ backToTopClassName={backToTopClassName}
+ className={styles.footer}
+ />
+ <noscript>
+ <NoScript message={noScript} position="top" />
+ </noscript>
+ </>
+ );
+};
+
+/**
+ * Get the global layout.
+ *
+ * @param {ReactElement} page - A page.
+ * @param {boolean} [isHome] - Determine if it is the homepage.
+ * @returns A page wrapped with the global layout.
+ */
+export const getLayout = (
+ page: ReactElement,
+ props: NextPageWithLayoutOptions
+) => {
+ return <Layout {...props}>{page}</Layout>;
+};
+
+export default Layout;