From 947a06bfdfdc5bca62c27fa2ee27f0ab9fefa0ea Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 22 Apr 2022 18:33:04 +0200
Subject: chore: add a TableOfContents component

---
 .../organisms/widgets/table-of-contents.tsx        | 53 ++++++++++++++++++++++
 1 file changed, 53 insertions(+)
 create mode 100644 src/components/organisms/widgets/table-of-contents.tsx

(limited to 'src/components/organisms/widgets/table-of-contents.tsx')

diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx
new file mode 100644
index 0000000..3778e02
--- /dev/null
+++ b/src/components/organisms/widgets/table-of-contents.tsx
@@ -0,0 +1,53 @@
+import useHeadingsTree, { type Heading } from '@utils/hooks/use-headings-tree';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import LinksListWidget, { type LinksListItems } from './links-list-widget';
+
+type TableOfContentsProps = {
+  /**
+   * A reference to the HTML element that contains the headings.
+   */
+  wrapper: HTMLElement;
+};
+
+/**
+ * Table of Contents widget component
+ *
+ * Render a table of contents.
+ */
+const TableOfContents: FC<TableOfContentsProps> = ({ wrapper }) => {
+  const intl = useIntl();
+  const headingsTree = useHeadingsTree(wrapper);
+  const title = intl.formatMessage({
+    defaultMessage: 'Table of Contents',
+    description: 'TableOfContents: the widget title',
+    id: 'WKG9wj',
+  });
+
+  /**
+   * Convert an headings tree to list items.
+   *
+   * @param {Heading[]} tree - The headings tree.
+   * @returns {LinksListItems[]} The list items.
+   */
+  const getItems = (tree: Heading[]): LinksListItems[] => {
+    return tree.map((heading) => {
+      return {
+        name: heading.title,
+        url: `#${heading.id}`,
+        child: getItems(heading.children),
+      };
+    });
+  };
+
+  return (
+    <LinksListWidget
+      kind="ordered"
+      title={title}
+      level={2}
+      items={getItems(headingsTree)}
+    />
+  );
+};
+
+export default TableOfContents;
-- 
cgit v1.2.3


From 83a029084f1bbfd78b7099d9bea3371d4533c6d9 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Tue, 3 May 2022 16:51:22 +0200
Subject: chore: add a LegalNotice page

---
 mdx.d.ts                                           |  10 +-
 .../molecules/buttons/heading-button.module.scss   |   4 +-
 src/components/molecules/layout/meta.module.scss   |   8 +-
 .../molecules/layout/page-header.module.scss       |   7 +-
 .../widgets/links-list-widget.module.scss          |  10 +-
 .../organisms/widgets/links-list-widget.tsx        |   5 +-
 .../widgets/table-of-contents.module.scss          |   4 +
 .../organisms/widgets/table-of-contents.tsx        |   2 +
 src/pages/mentions-legales.tsx                     | 140 +++++++++++++++++++++
 src/ts/types/app.ts                                |  16 ++-
 src/ts/types/mdx.ts                                |  20 +++
 src/ts/types/raw-data.ts                           |   6 +-
 src/utils/helpers/author.ts                        |  10 +-
 13 files changed, 209 insertions(+), 33 deletions(-)
 create mode 100644 src/components/organisms/widgets/table-of-contents.module.scss
 create mode 100644 src/pages/mentions-legales.tsx
 create mode 100644 src/ts/types/mdx.ts

(limited to 'src/components/organisms/widgets/table-of-contents.tsx')

diff --git a/mdx.d.ts b/mdx.d.ts
index f3a9a90..b4e333d 100644
--- a/mdx.d.ts
+++ b/mdx.d.ts
@@ -1,13 +1,9 @@
 declare module '*.mdx' {
+  import { MDXData, MDXPageMeta, MDXProjectMeta } from '@ts/types/mdx';
   import { MDXProps } from 'mdx/types';
-  import { Meta } from '@ts/types/app';
 
   let MDXComponent: (props: MDXProps) => JSX.Element;
   export default MDXComponent;
-  export const cover: string;
-  export const image: string;
-  export const intro: string;
-  export const meta: Meta;
-  export const pdf: string;
-  export const seo: { title: string; description: string };
+  export const data: MDXData;
+  export const meta: MDXPageMeta | MDXProjectMeta;
 }
diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss
index 1d16410..9c278e4 100644
--- a/src/components/molecules/buttons/heading-button.module.scss
+++ b/src/components/molecules/buttons/heading-button.module.scss
@@ -36,6 +36,8 @@
 }
 
 .heading {
-  background: none;
   padding: var(--spacing-2xs) 0;
+  background: none;
+  font-size: var(--font-size-xl);
+  text-align: left;
 }
diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss
index f7cc55b..0485545 100644
--- a/src/components/molecules/layout/meta.module.scss
+++ b/src/components/molecules/layout/meta.module.scss
@@ -2,12 +2,18 @@
 
 .list {
   display: grid;
-  grid-template-columns: repeat(2, minmax(0, 1fr));
+  grid-template-columns: repeat(1, minmax(0, 1fr));
+  gap: var(--spacing-sm);
 
   @include mix.media("screen") {
+    @include mix.dimensions("2xs") {
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+    }
+
     @include mix.dimensions("sm") {
       display: flex;
       flex-flow: column nowrap;
+      gap: var(--spacing-2xs);
     }
   }
 }
diff --git a/src/components/molecules/layout/page-header.module.scss b/src/components/molecules/layout/page-header.module.scss
index 93f7595..4c7df5f 100644
--- a/src/components/molecules/layout/page-header.module.scss
+++ b/src/components/molecules/layout/page-header.module.scss
@@ -1,5 +1,4 @@
 @use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
 @use "@styles/abstracts/placeholders";
 
 .wrapper {
@@ -55,9 +54,5 @@
 }
 
 .meta {
-  @include mix.media("screen") {
-    @include mix.dimensions("xs") {
-      font-size: var(--font-size-sm);
-    }
-  }
+  font-size: var(--font-size-sm);
 }
diff --git a/src/components/organisms/widgets/links-list-widget.module.scss b/src/components/organisms/widgets/links-list-widget.module.scss
index cbad83e..4444df4 100644
--- a/src/components/organisms/widgets/links-list-widget.module.scss
+++ b/src/components/organisms/widgets/links-list-widget.module.scss
@@ -3,6 +3,12 @@
 
 .widget {
   .list {
+    .list {
+      > *:first-child {
+        border-top: fun.convert-px(1) solid var(--color-primary);
+      }
+    }
+
     &__link {
       display: block;
       padding: var(--spacing-2xs) var(--spacing-xs);
@@ -50,9 +56,7 @@
 
     &__item {
       &:not(:last-child) {
-        .list__link {
-          border-bottom: fun.convert-px(1) solid var(--color-primary);
-        }
+        border-bottom: fun.convert-px(1) solid var(--color-primary);
       }
 
       > .list {
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
index 559d0b6..37a20fc 100644
--- a/src/components/organisms/widgets/links-list-widget.tsx
+++ b/src/components/organisms/widgets/links-list-widget.tsx
@@ -24,7 +24,7 @@ export type LinksListItems = {
 };
 
 export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> &
-  Pick<ListProps, 'kind'> & {
+  Pick<ListProps, 'className' | 'kind'> & {
     /**
      * An array of name/url couple.
      */
@@ -37,6 +37,7 @@ export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> &
  * Render a list of links inside a widget.
  */
 const LinksListWidget: FC<LinksListWidgetProps> = ({
+  className = '',
   items,
   kind = 'unordered',
   ...props
@@ -74,7 +75,7 @@ const LinksListWidget: FC<LinksListWidgetProps> = ({
         items={getListItems(items)}
         kind={kind}
         withMargin={false}
-        className={`${styles.list} ${styles[listKindClass]}`}
+        className={`${styles.list} ${styles[listKindClass]} ${className}`}
         itemsClassName={styles.list__item}
       />
     </Widget>
diff --git a/src/components/organisms/widgets/table-of-contents.module.scss b/src/components/organisms/widgets/table-of-contents.module.scss
new file mode 100644
index 0000000..36217ed
--- /dev/null
+++ b/src/components/organisms/widgets/table-of-contents.module.scss
@@ -0,0 +1,4 @@
+.list {
+  font-size: var(--font-size-sm);
+  font-weight: 500;
+}
diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx
index 3778e02..800ff58 100644
--- a/src/components/organisms/widgets/table-of-contents.tsx
+++ b/src/components/organisms/widgets/table-of-contents.tsx
@@ -2,6 +2,7 @@ import useHeadingsTree, { type Heading } from '@utils/hooks/use-headings-tree';
 import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import LinksListWidget, { type LinksListItems } from './links-list-widget';
+import styles from './table-of-contents.module.scss';
 
 type TableOfContentsProps = {
   /**
@@ -46,6 +47,7 @@ const TableOfContents: FC<TableOfContentsProps> = ({ wrapper }) => {
       title={title}
       level={2}
       items={getItems(headingsTree)}
+      className={styles.list}
     />
   );
 };
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
new file mode 100644
index 0000000..8dd0a1d
--- /dev/null
+++ b/src/pages/mentions-legales.tsx
@@ -0,0 +1,140 @@
+import Link from '@components/atoms/links/link';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
+import PageLayout, {
+  type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx';
+import { getFormattedDate } from '@utils/helpers/dates';
+import { loadTranslation } from '@utils/helpers/i18n';
+import useSettings from '@utils/hooks/use-settings';
+import { NestedMDXComponents } from 'mdx/types';
+import { GetStaticProps, NextPage } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { useIntl } from 'react-intl';
+import { Article, Graph, WebPage } from 'schema-dts';
+
+/**
+ * Legal Notice page.
+ */
+const LegalNoticePage: NextPage = () => {
+  const intl = useIntl();
+  const { dates, intro, seo, title } = meta;
+  const homeLabel = intl.formatMessage({
+    defaultMessage: 'Home',
+    description: 'Breadcrumb: home label',
+    id: 'j5k9Fe',
+  });
+  const breadcrumb: BreadcrumbItem[] = [
+    { id: 'home', name: homeLabel, url: '/' },
+    { id: 'legal-notice', name: title, url: '/mentions-legales' },
+  ];
+
+  const publicationLabel = intl.formatMessage({
+    defaultMessage: 'Published on:',
+    description: 'Meta: publication date label',
+    id: 'QGi5uD',
+  });
+
+  const updateLabel = intl.formatMessage({
+    defaultMessage: 'Updated on:',
+    description: 'Meta: update date label',
+    id: 'tLC7bh',
+  });
+
+  const headerMeta: PageLayoutProps['headerMeta'] = {
+    publication: {
+      name: publicationLabel,
+      value: getFormattedDate(dates.publication),
+    },
+    update: { name: updateLabel, value: getFormattedDate(dates.update) },
+  };
+
+  const components: NestedMDXComponents = {
+    Image: (props) => <ResponsiveImage {...props} />,
+    Link: (props) => <Link {...props} />,
+  };
+
+  const { website } = useSettings();
+  const { asPath } = useRouter();
+  const pageUrl = `${website.url}${asPath}`;
+  const pagePublicationDate = new Date(dates.publication);
+  const pageUpdateDate = new Date(dates.update);
+
+  const webpageSchema: WebPage = {
+    '@id': `${pageUrl}`,
+    '@type': 'WebPage',
+    breadcrumb: { '@id': `${website.url}/#breadcrumb` },
+    name: seo.title,
+    description: seo.description,
+    inLanguage: website.locales.default,
+    license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+    reviewedBy: { '@id': `${website.url}/#branding` },
+    url: `${pageUrl}`,
+    isPartOf: {
+      '@id': `${website.url}`,
+    },
+  };
+
+  const articleSchema: Article = {
+    '@id': `${website.url}/#legal-notice`,
+    '@type': 'Article',
+    name: title,
+    description: intro,
+    author: { '@id': `${website.url}/#branding` },
+    copyrightYear: pagePublicationDate.getFullYear(),
+    creator: { '@id': `${website.url}/#branding` },
+    dateCreated: pagePublicationDate.toISOString(),
+    dateModified: pageUpdateDate.toISOString(),
+    datePublished: pagePublicationDate.toISOString(),
+    editor: { '@id': `${website.url}/#branding` },
+    headline: title,
+    inLanguage: website.locales.default,
+    license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+    mainEntityOfPage: { '@id': `${pageUrl}` },
+  };
+
+  const schemaJsonLd: Graph = {
+    '@context': 'https://schema.org',
+    '@graph': [webpageSchema, articleSchema],
+  };
+
+  return (
+    <PageLayout
+      title={title}
+      intro={intro}
+      headerMeta={headerMeta}
+      breadcrumb={breadcrumb}
+      withToC={true}
+    >
+      <Head>
+        <title>{`${seo.title} - ${website.name}`}</title>
+        <meta name="description" content={seo.description} />
+        <meta property="og:url" content={`${pageUrl}`} />
+        <meta property="og:type" content="article" />
+        <meta property="og:title" content={`${seo.title} - ${website.name}`} />
+        <meta property="og:description" content={intro} />
+      </Head>
+      <Script
+        id="schema-legal-notice"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+      />
+      <LegalNoticeContent components={components} />
+    </PageLayout>
+  );
+};
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
+  const translation = await loadTranslation(locale);
+
+  return {
+    props: {
+      translation,
+    },
+  };
+};
+
+export default LegalNoticePage;
diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts
index b09f3d5..4d9c71c 100644
--- a/src/ts/types/app.ts
+++ b/src/ts/types/app.ts
@@ -1,8 +1,14 @@
-export type AuthorKind = 'page' | 'comment';
+export type ContentKind =
+  | 'article'
+  | 'comment'
+  | 'page'
+  | 'project'
+  | 'thematic'
+  | 'topic';
 
-export type Author<T extends AuthorKind> = {
+export type Author<T extends ContentKind> = {
   avatar?: Image;
-  description?: T extends 'page' ? string | undefined : never;
+  description?: T extends 'comment' ? never : string;
   name: string;
   website?: string;
 };
@@ -44,11 +50,11 @@ export type SEO = {
   title: string;
 };
 
-export type PageKind = 'article' | 'project' | 'thematic' | 'topic';
+export type PageKind = Exclude<ContentKind, 'comment'>;
 
 export type Meta<T extends PageKind> = {
   articles?: T extends 'thematic' | 'topic' ? Article[] : never;
-  author: Author<'page'>;
+  author?: T extends 'article' | 'page' ? Author<T> : never;
   commentsCount?: T extends 'article' ? number : never;
   cover?: Image;
   dates: Dates;
diff --git a/src/ts/types/mdx.ts b/src/ts/types/mdx.ts
new file mode 100644
index 0000000..6b72f21
--- /dev/null
+++ b/src/ts/types/mdx.ts
@@ -0,0 +1,20 @@
+import { StaticImageData } from 'next/image';
+import { Meta } from './app';
+
+export type MDXData = {
+  file: string;
+  image: StaticImageData;
+};
+
+export type MDXPageMeta = Pick<Meta<'page'>, 'cover' | 'dates' | 'seo'> & {
+  intro: string;
+  title: string;
+};
+
+export type MDXProjectMeta = Omit<
+  Meta<'project'>,
+  'readingTime' | 'wordsCount'
+> & {
+  intro: string;
+  title: string;
+};
diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts
index 43a2453..7e12e7f 100644
--- a/src/ts/types/raw-data.ts
+++ b/src/ts/types/raw-data.ts
@@ -3,7 +3,7 @@
  */
 
 import { NodeResponse, PageInfo } from '@services/graphql/api';
-import { AuthorKind } from './app';
+import { ContentKind } from './app';
 
 export type ACFPosts = {
   postsInThematic?: RawThematicPreview[];
@@ -29,8 +29,8 @@ export type Info = {
   wordsCount: number;
 };
 
-export type RawAuthor<T extends AuthorKind> = {
-  description?: T extends 'page' ? string | undefined : never;
+export type RawAuthor<T extends ContentKind> = {
+  description?: T extends 'comment' ? never : string;
   gravatarUrl?: string;
   name: string;
   url?: string;
diff --git a/src/utils/helpers/author.ts b/src/utils/helpers/author.ts
index cf125fc..40743ca 100644
--- a/src/utils/helpers/author.ts
+++ b/src/utils/helpers/author.ts
@@ -1,17 +1,17 @@
-import { type Author, type AuthorKind } from '@ts/types/app';
+import { type Author, type ContentKind } from '@ts/types/app';
 import { type RawAuthor } from '@ts/types/raw-data';
 
 /**
  * Convert author raw data to regular data.
  *
- * @param {RawAuthor<AuthorKind>} data - The author raw data.
- * @param {AuthorKind} kind - The author kind. Either `page` or `comment`.
+ * @param {RawAuthor<ContentKind>} data - The author raw data.
+ * @param {ContentKind} kind - The author kind. Either `page` or `comment`.
  * @param {number} [avatarSize] - The author avatar size.
- * @returns {Author<AuthorKind>} The author data.
+ * @returns {Author<ContentKind>} The author data.
  */
 export const getAuthorFromRawData = (
   data: RawAuthor<typeof kind>,
-  kind: AuthorKind,
+  kind: ContentKind,
   avatarSize: number = 80
 ): Author<typeof kind> => {
   const { name, description, gravatarUrl, url } = data;
-- 
cgit v1.2.3

