diff options
Diffstat (limited to 'src/components/molecules')
| -rw-r--r-- | src/components/molecules/layout/card.module.scss | 39 | ||||
| -rw-r--r-- | src/components/molecules/layout/card.stories.tsx | 38 | ||||
| -rw-r--r-- | src/components/molecules/layout/card.test.tsx | 13 | ||||
| -rw-r--r-- | src/components/molecules/layout/card.tsx | 16 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.module.scss | 18 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.stories.tsx | 57 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.test.tsx | 22 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.tsx | 304 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-footer.stories.tsx | 12 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-footer.tsx | 4 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-header.stories.tsx | 34 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-header.tsx | 25 | 
12 files changed, 450 insertions, 132 deletions
| diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss index d5b9836..3af8f24 100644 --- a/src/components/molecules/layout/card.module.scss +++ b/src/components/molecules/layout/card.module.scss @@ -52,24 +52,35 @@      margin-bottom: var(--spacing-md);    } -  .items { -    flex-flow: row wrap; -    place-content: center; -    gap: var(--spacing-2xs); -  } +  .meta { +    &__item { +      flex-flow: row wrap; +      place-content: center; +      gap: var(--spacing-2xs); +      margin: auto; +    } + +    &__label { +      flex: 0 0 100%; +    } -  .term { -    flex: 0 0 100%; +    &__value { +      padding: fun.convert-px(2) var(--spacing-xs); +      border: fun.convert-px(1) solid var(--color-primary-darker); +      color: var(--color-fg); +      font-weight: 400; + +      &::before { +        display: none; +      } +    }    } -  .description { -    padding: fun.convert-px(2) var(--spacing-xs); -    border: fun.convert-px(1) solid var(--color-primary-darker); -    color: var(--color-fg); -    font-weight: 400; +  &:not(:disabled):focus { +    text-decoration: none; -    &::before { -      display: none; +    .title { +      text-decoration: underline solid var(--color-primary) 0.3ex;      }    }  } diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx index ed78d00..2e99bbb 100644 --- a/src/components/molecules/layout/card.stories.tsx +++ b/src/components/molecules/layout/card.stories.tsx @@ -8,6 +8,19 @@ export default {    title: 'Molecules/Layout/Card',    component: Card,    argTypes: { +    className: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the card wrapper.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    },      cover: {        description: 'The card cover data (src, dimensions, alternative text).',        table: { @@ -19,6 +32,21 @@ export default {          value: {},        },      }, +    coverFit: { +      control: { +        type: 'select', +      }, +      description: 'The cover fit.', +      options: ['contain', 'cover', 'fill', 'scale-down'], +      table: { +        category: 'Options', +        defaultValue: { summary: 'cover' }, +      }, +      type: { +        name: 'string', +        required: false, +      }, +    },      meta: {        description: 'The card metadata (a publication date for example).',        table: { @@ -88,13 +116,9 @@ const cover = {    unoptimized: true,  }; -const meta = [ -  { -    id: 'an-id', -    term: 'Voluptates', -    value: ['Autem', 'Eos'], -  }, -]; +const meta = { +  thematics: ['Autem', 'Eos'], +};  /**   * Card Stories - Default diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx index 404bc7a..07c01e9 100644 --- a/src/components/molecules/layout/card.test.tsx +++ b/src/components/molecules/layout/card.test.tsx @@ -8,13 +8,10 @@ const cover = {    width: 640,  }; -const meta = [ -  { -    id: 'an-id', -    term: 'Voluptates', -    value: ['Autem', 'Eos'], -  }, -]; +const meta = { +  author: 'Possimus', +  thematics: ['Autem', 'Eos'], +};  const tagline = 'Ut rerum incidunt'; @@ -47,6 +44,6 @@ describe('Card', () => {    it('renders some meta', () => {      render(<Card title={title} titleLevel={2} url={url} meta={meta} />); -    expect(screen.getByText(meta[0].term)).toBeInTheDocument(); +    expect(screen.getByText(meta.author)).toBeInTheDocument();    });  }); diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx index 89f100e..e416bd5 100644 --- a/src/components/molecules/layout/card.tsx +++ b/src/components/molecules/layout/card.tsx @@ -1,13 +1,11 @@  import ButtonLink from '@components/atoms/buttons/button-link';  import Heading, { type HeadingLevel } from '@components/atoms/headings/heading'; -import DescriptionList, { -  type DescriptionListItem, -} from '@components/atoms/lists/description-list';  import { FC } from 'react';  import ResponsiveImage, {    type ResponsiveImageProps,  } from '../images/responsive-image';  import styles from './card.module.scss'; +import Meta, { type MetaData } from './meta';  export type Cover = {    /** @@ -44,7 +42,7 @@ export type CardProps = {    /**     * The card meta.     */ -  meta?: DescriptionListItem[]; +  meta?: MetaData;    /**     * The card tagline.     */ @@ -96,13 +94,13 @@ const Card: FC<CardProps> = ({          <div className={styles.tagline}>{tagline}</div>          {meta && (            <footer className={styles.footer}> -            <DescriptionList -              items={meta} +            <Meta +              data={meta}                layout="inline"                className={styles.list} -              groupClassName={styles.items} -              termClassName={styles.term} -              descriptionClassName={styles.description} +              groupClassName={styles.meta__item} +              labelClassName={styles.meta__label} +              valueClassName={styles.meta__value}              />            </footer>          )} diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss index 0485545..4194a6e 100644 --- a/src/components/molecules/layout/meta.module.scss +++ b/src/components/molecules/layout/meta.module.scss @@ -1,23 +1,5 @@  @use "@styles/abstracts/mixins" as mix; -.list { -  display: grid; -  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); -    } -  } -} -  .value {    word-break: break-all;  } diff --git a/src/components/molecules/layout/meta.stories.tsx b/src/components/molecules/layout/meta.stories.tsx index 0323f90..a1755a0 100644 --- a/src/components/molecules/layout/meta.stories.tsx +++ b/src/components/molecules/layout/meta.stories.tsx @@ -1,5 +1,5 @@  import { ComponentMeta, ComponentStory } from '@storybook/react'; -import MetaComponent from './meta'; +import MetaComponent, { MetaData } from './meta';  /**   * Meta - Storybook Meta @@ -8,25 +8,41 @@ export default {    title: 'Molecules/Layout',    component: MetaComponent,    argTypes: { -    className: { +    data: { +      description: 'The page metadata.', +      type: { +        name: 'object', +        required: true, +        value: {}, +      }, +    }, +    itemsLayout: {        control: { -        type: 'text', +        type: 'select',        }, -      description: 'Set additional classnames to the meta wrapper.', +      description: 'The items layout.', +      options: ['inline', 'inline-values', 'stacked'],        table: { -        category: 'Styles', +        category: 'Options', +        defaultValue: { summary: 'inline-values' },        },        type: {          name: 'string',          required: false,        },      }, -    data: { -      description: 'The page metadata.', +    withSeparator: { +      control: { +        type: 'boolean', +      }, +      description: 'Add a slash as separator between multiple values.', +      table: { +        category: 'Options', +        defaultValue: { summary: true }, +      },        type: { -        name: 'object', +        name: 'boolean',          required: true, -        value: {},        },      },    }, @@ -36,19 +52,16 @@ const Template: ComponentStory<typeof MetaComponent> = (args) => (    <MetaComponent {...args} />  ); -const data = { -  publication: { name: 'Published on:', value: 'April 9th 2022' }, -  categories: { -    name: 'Categories:', -    value: [ -      <a key="category1" href="#"> -        Category 1 -      </a>, -      <a key="category2" href="#"> -        Category 2 -      </a>, -    ], -  }, +const data: MetaData = { +  publication: { date: '2022-04-09', time: '01:04:00' }, +  thematics: [ +    <a key="category1" href="#"> +      Category 1 +    </a>, +    <a key="category2" href="#"> +      Category 2 +    </a>, +  ],  };  /** diff --git a/src/components/molecules/layout/meta.test.tsx b/src/components/molecules/layout/meta.test.tsx index a738bdb..fe66d97 100644 --- a/src/components/molecules/layout/meta.test.tsx +++ b/src/components/molecules/layout/meta.test.tsx @@ -1,8 +1,24 @@ -import { render } from '@test-utils'; +import { render, screen } from '@test-utils'; +import { getFormattedDate } from '@utils/helpers/dates';  import Meta from './meta'; +const data = { +  publication: { date: '2022-04-09' }, +  thematics: [ +    <a key="category1" href="#"> +      Category 1 +    </a>, +    <a key="category2" href="#"> +      Category 2 +    </a>, +  ], +}; +  describe('Meta', () => { -  it('renders a Meta component', () => { -    render(<Meta data={{}} />); +  it('format a date string', () => { +    render(<Meta data={data} />); +    expect( +      screen.getByText(getFormattedDate(data.publication.date)) +    ).toBeInTheDocument();    });  }); 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}      />    ); diff --git a/src/components/molecules/layout/page-footer.stories.tsx b/src/components/molecules/layout/page-footer.stories.tsx index da0a3fa..31b7a49 100644 --- a/src/components/molecules/layout/page-footer.stories.tsx +++ b/src/components/molecules/layout/page-footer.stories.tsx @@ -1,4 +1,5 @@  import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { MetaData } from './meta';  import PageFooterComponent from './page-footer';  /** @@ -39,8 +40,15 @@ const Template: ComponentStory<typeof PageFooterComponent> = (args) => (    <PageFooterComponent {...args} />  ); -const meta = { -  topics: { name: 'More posts about:', value: <a href="#">Topic name</a> }, +const meta: MetaData = { +  custom: { +    label: 'More posts about:', +    value: [ +      <a key="topic-1" href="#"> +        Topic name +      </a>, +    ], +  },  };  /** diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx index f522482..e998b1e 100644 --- a/src/components/molecules/layout/page-footer.tsx +++ b/src/components/molecules/layout/page-footer.tsx @@ -1,5 +1,5 @@  import { FC } from 'react'; -import Meta, { type MetaMap } from './meta'; +import Meta, { MetaData } from './meta';  export type PageFooterProps = {    /** @@ -9,7 +9,7 @@ export type PageFooterProps = {    /**     * The footer metadata.     */ -  meta?: MetaMap; +  meta?: MetaData;  };  /** diff --git a/src/components/molecules/layout/page-header.stories.tsx b/src/components/molecules/layout/page-header.stories.tsx index 6054845..d58f8b5 100644 --- a/src/components/molecules/layout/page-header.stories.tsx +++ b/src/components/molecules/layout/page-header.stories.tsx @@ -8,6 +8,19 @@ export default {    title: 'Molecules/Layout/PageHeader',    component: PageHeader,    argTypes: { +    className: { +      control: { +        type: 'text', +      }, +      description: 'Set additional classnames to the header element.', +      table: { +        category: 'Styles', +      }, +      type: { +        name: 'string', +        required: false, +      }, +    },      intro: {        control: {          type: 'text', @@ -50,18 +63,15 @@ const Template: ComponentStory<typeof PageHeader> = (args) => (  );  const meta = { -  publication: { name: 'Published on:', value: 'April 9th 2022' }, -  categories: { -    name: 'Categories:', -    value: [ -      <a key="category1" href="#"> -        Category 1 -      </a>, -      <a key="category2" href="#"> -        Category 2 -      </a>, -    ], -  }, +  publication: { date: '2022-04-09' }, +  thematics: [ +    <a key="category1" href="#"> +      Category 1 +    </a>, +    <a key="category2" href="#"> +      Category 2 +    </a>, +  ],  };  /** diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx index 1663085..9abe9af 100644 --- a/src/components/molecules/layout/page-header.tsx +++ b/src/components/molecules/layout/page-header.tsx @@ -1,7 +1,7 @@  import Heading from '@components/atoms/headings/heading'; -import styles from './page-header.module.scss'; -import Meta, { type MetaMap } from './meta';  import { FC } from 'react'; +import Meta, { type MetaData } from './meta'; +import styles from './page-header.module.scss';  export type PageHeaderProps = {    /** @@ -15,7 +15,7 @@ export type PageHeaderProps = {    /**     * The page metadata.     */ -  meta?: MetaMap; +  meta?: MetaData;    /**     * The page title.     */ @@ -33,14 +33,29 @@ const PageHeader: FC<PageHeaderProps> = ({    meta,    title,  }) => { +  const getIntro = () => { +    return typeof intro === 'string' ? ( +      <div dangerouslySetInnerHTML={{ __html: intro }} /> +    ) : ( +      <div>{intro}</div> +    ); +  }; +    return (      <header className={`${styles.wrapper} ${className}`}>        <div className={styles.body}>          <Heading level={1} className={styles.title} withMargin={false}>            {title}          </Heading> -        {meta && <Meta data={meta} className={styles.meta} layout="inline" />} -        {intro && <div>{intro}</div>} +        {meta && ( +          <Meta +            data={meta} +            className={styles.meta} +            layout="column" +            itemsLayout="inline" +          /> +        )} +        {intro && getIntro()}        </div>      </header>    ); | 
