summaryrefslogtreecommitdiffstats
path: root/src/components/atoms/lists/description-list-item.tsx
blob: 9505d014c52960feb3f2a7b67226ce1132f36180 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { FC, ReactNode, useId } from 'react';
import styles from './description-list-item.module.scss';

export type ItemLayout = 'inline' | 'inline-values' | 'stacked';

export type DescriptionListItemProps = {
  /**
   * Set additional classnames to the list item wrapper.
   */
  className?: string;
  /**
   * Set additional classnames to the list item description.
   */
  descriptionClassName?: string;
  /**
   * The item label.
   */
  label: string;
  /**
   * The item layout.
   */
  layout?: ItemLayout;
  /**
   * Set additional classnames to the list item term.
   */
  termClassName?: string;
  /**
   * The item value.
   */
  value: ReactNode | ReactNode[];
  /**
   * If true, use a slash to delimitate multiple values.
   */
  withSeparator?: boolean;
};

/**
 * DescriptionListItem component
 *
 * Render a couple of dt/dd wrapped in a div.
 */
const DescriptionListItem: FC<DescriptionListItemProps> = ({
  className = '',
  descriptionClassName = '',
  label,
  termClassName = '',
  value,
  layout = 'stacked',
  withSeparator = false,
}) => {
  const id = useId();
  const layoutStyles = styles[`wrapper--${layout}`];
  const separatorStyles = withSeparator ? styles['wrapper--has-separator'] : '';
  const itemValues = Array.isArray(value) ? value : [value];

  return (
    <div
      className={`${styles.wrapper} ${layoutStyles} ${separatorStyles} ${className}`}
    >
      <dt className={`${styles.term} ${termClassName}`}>{label}</dt>
      {itemValues.map((currentValue, index) => (
        <dd
          key={`${id}-${index}`}
          className={`${styles.description} ${descriptionClassName}`}
        >
          {currentValue}
        </dd>
      ))}
    </div>
  );
};

export default DescriptionListItem;
eight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
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
        title={name}
        baseline={baseline}
        photo={picture}
        nav={mainNav}
        searchPage="/recherche"
        isHome={isHome}
        withLink={true}
        className={styles.header}
      />
      <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;