diff options
Diffstat (limited to 'src/components/templates/layout/layout.tsx')
| -rw-r--r-- | src/components/templates/layout/layout.tsx | 242 |
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; |
