diff options
Diffstat (limited to 'src/components/templates')
| -rw-r--r-- | src/components/templates/layout/layout.module.scss | 35 | ||||
| -rw-r--r-- | src/components/templates/layout/layout.stories.tsx | 57 | ||||
| -rw-r--r-- | src/components/templates/layout/layout.test.tsx | 34 | ||||
| -rw-r--r-- | src/components/templates/layout/layout.tsx | 125 | 
4 files changed, 251 insertions, 0 deletions
| diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss new file mode 100644 index 0000000..eb84c70 --- /dev/null +++ b/src/components/templates/layout/layout.module.scss @@ -0,0 +1,35 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +:global { +  #__next { +    flex: 1; +    display: flex; +    flex-flow: column nowrap; +    min-height: 100vh; +  } +} + +.header { +  padding: var(--spacing-sm) 0 var(--spacing-md); +  border-bottom: fun.convert-px(3) solid var(--color-border-light); +} + +.main { +  flex: 1; +} + +.footer { +  border-top: fun.convert-px(3) solid var(--color-border-light); +} + +.noscript-spacing { +  width: 100%; +  height: fun.convert-px(75); + +  @include mix.media("screen") { +    @include mix.dimensions("xs") { +      height: fun.convert-px(50); +    } +  } +} diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx new file mode 100644 index 0000000..f3579e3 --- /dev/null +++ b/src/components/templates/layout/layout.stories.tsx @@ -0,0 +1,57 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import LayoutComponent from './layout'; + +/** + * Layout - Storybook Meta + */ +export default { +  title: 'Templates/LayoutBase', +  component: LayoutComponent, +  argTypes: { +    children: { +      control: { +        type: 'text', +      }, +      description: 'The article content.', +      type: { +        name: 'string', +        required: true, +      }, +    }, +    className: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the article element.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    }, +  }, +  decorators: [ +    (Story) => ( +      <IntlProvider locale="en"> +        <div id="__next"> +          <Story /> +        </div> +      </IntlProvider> +    ), +  ], +  parameters: { +    layout: 'fullscreen', +  }, +} as ComponentMeta<typeof LayoutComponent>; + +const Template: ComponentStory<typeof LayoutComponent> = (args) => ( +  <LayoutComponent {...args} /> +); + +/** + * Layout Stories - Default + */ +export const LayoutBase = Template.bind({}); diff --git a/src/components/templates/layout/layout.test.tsx b/src/components/templates/layout/layout.test.tsx new file mode 100644 index 0000000..914e1cd --- /dev/null +++ b/src/components/templates/layout/layout.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@test-utils'; +import Layout 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.'; + +describe('Layout', () => { +  it('renders the website header', () => { +    render(<Layout>{body}</Layout>); +    expect(screen.getByRole('banner')).toBeInTheDocument(); +  }); + +  it('renders the website main content', () => { +    render(<Layout>{body}</Layout>); +    expect(screen.getByRole('main')).toBeInTheDocument(); +  }); + +  it('renders the website footer', () => { +    render(<Layout>{body}</Layout>); +    expect(screen.getByRole('contentinfo')).toBeInTheDocument(); +  }); + +  it('renders a skip to content link', () => { +    render(<Layout>{body}</Layout>); +    expect( +      screen.getByRole('link', { name: 'Skip to content' }) +    ).toBeInTheDocument(); +  }); + +  it('renders an article', () => { +    render(<Layout>{body}</Layout>); +    expect(screen.getByRole('article')).toHaveTextContent(body); +  }); +}); diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx new file mode 100644 index 0000000..601ced4 --- /dev/null +++ b/src/components/templates/layout/layout.tsx @@ -0,0 +1,125 @@ +import photo from '@assets/images/armand-philippot.jpg'; +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 from '@components/organisms/layout/footer'; +import Header, { HeaderProps } from '@components/organisms/layout/header'; +import { settings } from '@utils/config'; +import { FC, ReactNode } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './layout.module.scss'; + +export type LayoutProps = Pick<HeaderProps, 'isHome'> & { +  /** +   * The layout main content. +   */ +  children: ReactNode; +  /** +   * Set additional classnames to the article element. +   */ +  className?: string; +}; + +/** + * Layout component + * + * Render the base layout used by all pages. + */ +const Layout: FC<LayoutProps> = ({ children, isHome, ...props }) => { +  const intl = useIntl(); +  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 copyright = { +    dates: { +      start: settings.copyright.startYear, +      end: settings.copyright.endYear, +    }, +    owner: settings.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 nav: HeaderProps['nav'] = [ +    { id: 'home', label: homeLabel, href: '#', logo: <Home /> }, +    { id: 'blog', label: blogLabel, href: '#', logo: <PostsStack /> }, +    { +      id: 'projects', +      label: projectsLabel, +      href: '#', +      logo: <ComputerScreen />, +    }, +    { id: 'cv', label: cvLabel, href: '#', logo: <Career /> }, +    { id: 'contact', label: contactLabel, href: '#', logo: <Envelop /> }, +  ]; + +  return ( +    <> +      <noscript> +        <div className={styles['noscript-spacing']}></div> +      </noscript> +      <span tabIndex={-1}></span> +      <ButtonLink target="#main" className="screen-reader-text"> +        {skipToContent} +      </ButtonLink> +      <Header +        title={settings.name} +        baseline={settings.baseline.fr} +        photo={photo.src} +        nav={nav} +        isHome={isHome} +        className={styles.header} +        withLink={true} +      /> +      <Main id="main" className={styles.main}> +        <article {...props}>{children}</article> +      </Main> +      <Footer copyright={copyright} topId="top" className={styles.footer} /> +      <noscript> +        <NoScript message={noScript} position="top" /> +      </noscript> +    </> +  ); +}; + +export default Layout; | 
