From 0d59a6d2995b4119865271ed1908ede0bb96497c Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Mon, 9 May 2022 18:19:38 +0200
Subject: refactor: rewrite DescriptionList and Meta components

The meta can have different layout. The previous implementation was not
enough to easily change the layout. Also, I prefer to restrict the meta
types and it prevents me to repeat myself for the labels.
---
 src/components/molecules/layout/meta.tsx | 304 ++++++++++++++++++++++++++++---
 1 file changed, 274 insertions(+), 30 deletions(-)

(limited to 'src/components/molecules/layout/meta.tsx')

diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
index d05396e..1401ac4 100644
--- a/src/components/molecules/layout/meta.tsx
+++ b/src/components/molecules/layout/meta.tsx
@@ -1,67 +1,312 @@
+import Link from '@components/atoms/links/link';
 import DescriptionList, {
   type DescriptionListProps,
   type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
+import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
 import { FC, ReactNode } from 'react';
-import styles from './meta.module.scss';
+import { useIntl } from 'react-intl';
 
-export type MetaItem = {
+export type CustomMeta = {
+  label: string;
+  value: ReactNode | ReactNode[];
+};
+
+export type MetaDate = {
   /**
-   * The meta name.
+   * A date string. Ex: `2022-04-30`.
    */
-  name: string;
+  date: string;
   /**
-   * The meta value.
+   * A time string. Ex: `10:25:59`.
    */
-  value: ReactNode | ReactNode[];
-};
-
-export type MetaMap = {
-  [key: string]: MetaItem | undefined;
+  time?: string;
+  /**
+   * Wrap the date with a link to the given target.
+   */
+  target?: string;
 };
 
-export type MetaProps = {
+export type MetaData = {
+  /**
+   * The author name.
+   */
+  author?: string;
+  /**
+   * The comments count.
+   */
+  commentsCount?: string | JSX.Element;
+  /**
+   * The creation date.
+   */
+  creation?: MetaDate;
+  /**
+   * A custom label/value metadata.
+   */
+  custom?: CustomMeta;
+  /**
+   * The license name.
+   */
+  license?: string;
+  /**
+   * The popularity.
+   */
+  popularity?: string | JSX.Element;
+  /**
+   * The publication date.
+   */
+  publication?: MetaDate;
+  /**
+   * The estimated reading time.
+   */
+  readingTime?: string | JSX.Element;
+  /**
+   * An array of repositories.
+   */
+  repositories?: string[] | JSX.Element[];
+  /**
+   * An array of technologies.
+   */
+  technologies?: string[];
   /**
-   * Set additional classnames to the meta wrapper.
+   * An array of thematics.
    */
-  className?: DescriptionListProps['className'];
+  thematics?: string[] | JSX.Element[];
+  /**
+   * An array of thematics.
+   */
+  topics?: string[] | JSX.Element[];
+  /**
+   * A total.
+   */
+  total?: string;
+  /**
+   * The update date.
+   */
+  update?: MetaDate;
+};
+
+export type MetaProps = Omit<
+  DescriptionListProps,
+  'items' | 'withSeparator'
+> & {
   /**
    * The meta data.
    */
-  data: MetaMap;
+  data: MetaData;
   /**
-   * The meta layout.
+   * The items layout.
    */
-  layout?: DescriptionListProps['layout'];
+  itemsLayout?: DescriptionListItem['layout'];
   /**
-   * Determine if the layout should be responsive.
+   * If true, use a slash to delimitate multiple values. Default: true.
    */
-  responsiveLayout?: DescriptionListProps['responsiveLayout'];
+  withSeparator?: DescriptionListProps['withSeparator'];
 };
 
 /**
  * Meta component
  *
- * Renders the page metadata.
+ * Renders the given metadata.
  */
-const Meta: FC<MetaProps> = ({ className, data, ...props }) => {
+const Meta: FC<MetaProps> = ({
+  data,
+  itemsLayout = 'inline-values',
+  withSeparator = true,
+  ...props
+}) => {
+  const intl = useIntl();
+
+  /**
+   * Retrieve the item label based on its key.
+   *
+   * @param {keyof MetaData} key - The meta key.
+   * @returns {string} The item label.
+   */
+  const getLabel = (key: keyof MetaData): string => {
+    switch (key) {
+      case 'author':
+        return intl.formatMessage({
+          defaultMessage: 'Written by:',
+          id: 'OI0N37',
+          description: 'Meta: author label',
+        });
+      case 'commentsCount':
+        return intl.formatMessage({
+          defaultMessage: 'Comments:',
+          id: 'jTVIh8',
+          description: 'Meta: comments label',
+        });
+      case 'creation':
+        return intl.formatMessage({
+          defaultMessage: 'Created on:',
+          id: 'b4fdYE',
+          description: 'Meta: creation date label',
+        });
+      case 'license':
+        return intl.formatMessage({
+          defaultMessage: 'License:',
+          id: 'AuGklx',
+          description: 'Meta: license label',
+        });
+      case 'popularity':
+        return intl.formatMessage({
+          defaultMessage: 'Popularity:',
+          id: 'pWTj2W',
+          description: 'Meta: popularity label',
+        });
+      case 'publication':
+        return intl.formatMessage({
+          defaultMessage: 'Published on:',
+          id: 'QGi5uD',
+          description: 'Meta: publication date label',
+        });
+      case 'readingTime':
+        return intl.formatMessage({
+          defaultMessage: 'Reading time:',
+          id: 'EbFvsM',
+          description: 'Meta: reading time label',
+        });
+      case 'repositories':
+        return intl.formatMessage({
+          defaultMessage: 'Repositories:',
+          id: 'DssFG1',
+          description: 'Meta: repositories label',
+        });
+      case 'technologies':
+        return intl.formatMessage({
+          defaultMessage: 'Technologies:',
+          id: 'ADQmDF',
+          description: 'Meta: technologies label',
+        });
+      case 'thematics':
+        return intl.formatMessage({
+          defaultMessage: 'Thematics:',
+          id: 'bz53Us',
+          description: 'Meta: thematics label',
+        });
+      case 'topics':
+        return intl.formatMessage({
+          defaultMessage: 'Topics:',
+          id: 'gJNaBD',
+          description: 'Meta: topics label',
+        });
+      case 'total':
+        return intl.formatMessage({
+          defaultMessage: 'Total:',
+          id: '92zgdp',
+          description: 'Meta: total label',
+        });
+      case 'update':
+        return intl.formatMessage({
+          defaultMessage: 'Updated on:',
+          id: 'tLC7bh',
+          description: 'Meta: update date label',
+        });
+      default:
+        return '';
+    }
+  };
+
+  /**
+   * Retrieve a formatted date (and time).
+   *
+   * @param {MetaDate} dateTime - A date object.
+   * @returns {JSX.Element} The formatted date wrapped in a time element.
+   */
+  const getDate = (dateTime: MetaDate): JSX.Element => {
+    const { date, time, target } = dateTime;
+
+    if (!dateTime.time) {
+      const isoDate = new Date(`${date}`).toISOString();
+      return target ? (
+        <Link href={target}>
+          <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time>
+        </Link>
+      ) : (
+        <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time>
+      );
+    }
+
+    const isoDateTime = new Date(`${date}T${time}`).toISOString();
+
+    return target ? (
+      <Link href={target}>
+        <time dateTime={isoDateTime}>
+          {intl.formatMessage(
+            {
+              defaultMessage: '{date} at {time}',
+              description: 'Meta: publication date and time',
+              id: 'fcHeyC',
+            },
+            {
+              date: getFormattedDate(dateTime.date),
+              time: getFormattedTime(`${dateTime.date}T${dateTime.time}`),
+            }
+          )}
+        </time>
+      </Link>
+    ) : (
+      <time dateTime={isoDateTime}>
+        {intl.formatMessage(
+          {
+            defaultMessage: '{date} at {time}',
+            description: 'Meta: publication date and time',
+            id: 'fcHeyC',
+          },
+          {
+            date: getFormattedDate(dateTime.date),
+            time: getFormattedTime(`${dateTime.date}T${dateTime.time}`),
+          }
+        )}
+      </time>
+    );
+  };
+
+  /**
+   * Retrieve the formatted item value.
+   *
+   * @param {keyof MetaData} key - The meta key.
+   * @param {ValueOf<MetaData>} value - The meta value.
+   * @returns {string|ReactNode|ReactNode[]} - The formatted value.
+   */
+  const getValue = <T extends keyof MetaData>(
+    key: T,
+    value: MetaData[T]
+  ): string | ReactNode | ReactNode[] => {
+    if (key === 'creation' || key === 'publication' || key === 'update') {
+      return getDate(value as MetaDate);
+    }
+    return value as string | ReactNode | ReactNode[];
+  };
+
   /**
    * Transform the metadata to description list item format.
    *
-   * @param {MetaMap} items - The meta.
+   * @param {MetaData} items - The meta.
    * @returns {DescriptionListItem[]} The formatted description list items.
    */
-  const getItems = (items: MetaMap): DescriptionListItem[] => {
+  const getItems = (items: MetaData): DescriptionListItem[] => {
     const listItems: DescriptionListItem[] = Object.entries(items)
-      .map(([key, item]) => {
-        if (!item) return;
+      .map(([key, value]) => {
+        if (!key || !value) return;
 
-        const { name, value } = item;
+        const metaKey = key as keyof MetaData;
 
         return {
-          id: key,
-          term: name,
-          value: Array.isArray(value) ? value : [value],
+          id: metaKey,
+          label:
+            metaKey === 'custom'
+              ? (value as CustomMeta).label
+              : getLabel(metaKey),
+          layout: itemsLayout,
+          value:
+            metaKey === 'custom'
+              ? (value as CustomMeta).value
+              : getValue(
+                  metaKey,
+                  value as string | string[] | JSX.Element | JSX.Element[]
+                ),
         } as DescriptionListItem;
       })
       .filter((item): item is DescriptionListItem => !!item);
@@ -72,8 +317,7 @@ const Meta: FC<MetaProps> = ({ className, data, ...props }) => {
   return (
     <DescriptionList
       items={getItems(data)}
-      className={`${styles.list} ${className}`}
-      descriptionClassName={styles.value}
+      withSeparator={withSeparator}
       {...props}
     />
   );
-- 
cgit v1.2.3

