summaryrefslogtreecommitdiffstats
path: root/src/components/PostFooter/PostFooter.tsx
blob: 8c09d690f4ee5646fe5d347690582e3021f8789b (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
import { t } from '@lingui/macro';
import { SubjectPreview } from '@ts/types/taxonomies';
import Link from 'next/link';
import styles from './PostFooter.module.scss';

const PostFooter = ({ subjects }: { subjects: SubjectPreview[] }) => {
  const getSubjects = () => {
    return subjects.map((subject) => {
      return (
        <li key={subject.id}>
          <Link href={`/sujet/${subject.slug}`}>
            <a>{subject.title}</a>
          </Link>
        </li>
      );
    });
  };

  return (
    <footer>
      {subjects.length > 0 && (
        <>
          <dl className={styles.meta}>
            <dt>{t`Subjects:`}</dt>
            <dd>
              <ul className={styles.list}>{getSubjects()}</ul>
            </dd>
          </dl>
        </>
      )}
    </footer>
  );
};

export default PostFooter;
#n231'>231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
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 { useIntl } from 'react-intl';

export type CustomMeta = {
  label: string;
  value: ReactNode | ReactNode[];
};

export type MetaComments = {
  /**
   * A page title.
   */
  about: string;
  /**
   * The comments count.
   */
  count: number;
  /**
   * Wrap the comments count with a link to the given target.
   */
  target?: string;
};

export type MetaDate = {
  /**
   * A date string. Ex: `2022-04-30`.
   */
  date: string;
  /**
   * A time string. Ex: `10:25:59`.
   */
  time?: string;
  /**
   * Wrap the date with a link to the given target.
   */
  target?: string;
};

export type MetaData = {
  /**
   * The author name.
   */
  author?: string;
  /**
   * The comments count.
   */
  comments?: MetaComments;
  /**
   * 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[];
  /**
   * An array of thematics.
   */
  thematics?: string[] | JSX.Element[];
  /**
   * An array of thematics.
   */
  topics?: string[] | JSX.Element[];
  /**
   * A total number of posts.
   */
  total?: number;
  /**
   * The update date.
   */
  update?: MetaDate;
  /**
   * An url.
   */
  website?: string;
};

export type MetaKey = keyof MetaData;

export type MetaProps = Omit<
  DescriptionListProps,
  'items' | 'withSeparator'
> & {
  /**
   * The meta data.
   */
  data: MetaData;
  /**
   * The items layout.
   */
  itemsLayout?: DescriptionListItem['layout'];
  /**
   * If true, use a slash to delimitate multiple values. Default: true.
   */
  withSeparator?: DescriptionListProps['withSeparator'];
};

/**
 * Meta component
 *
 * Renders the given metadata.
 */
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:',
          description: 'Meta: author label',
          id: 'OI0N37',
        });
      case 'comments':
        return intl.formatMessage({
          defaultMessage: 'Comments:',
          description: 'Meta: comments label',
          id: 'jTVIh8',
        });
      case 'creation':
        return intl.formatMessage({
          defaultMessage: 'Created on:',
          description: 'Meta: creation date label',
          id: 'b4fdYE',
        });
      case 'license':
        return intl.formatMessage({
          defaultMessage: 'License:',
          description: 'Meta: license label',
          id: 'AuGklx',
        });
      case 'popularity':
        return intl.formatMessage({
          defaultMessage: 'Popularity:',
          description: 'Meta: popularity label',
          id: 'pWTj2W',
        });
      case 'publication':
        return intl.formatMessage({
          defaultMessage: 'Published on:',
          description: 'Meta: publication date label',
          id: 'QGi5uD',
        });
      case 'readingTime':
        return intl.formatMessage({
          defaultMessage: 'Reading time:',
          description: 'Meta: reading time label',
          id: 'EbFvsM',
        });
      case 'repositories':
        return intl.formatMessage({
          defaultMessage: 'Repositories:',
          description: 'Meta: repositories label',
          id: 'DssFG1',
        });
      case 'technologies':
        return intl.formatMessage({
          defaultMessage: 'Technologies:',
          description: 'Meta: technologies label',
          id: 'ADQmDF',
        });
      case 'thematics':
        return intl.formatMessage({
          defaultMessage: 'Thematics:',
          description: 'Meta: thematics label',
          id: 'bz53Us',
        });
      case 'topics':
        return intl.formatMessage({
          defaultMessage: 'Topics:',
          description: 'Meta: topics label',
          id: 'gJNaBD',
        });
      case 'total':
        return intl.formatMessage({
          defaultMessage: 'Total:',
          description: 'Meta: total label',
          id: '92zgdp',
        });
      case 'update':
        return intl.formatMessage({
          defaultMessage: 'Updated on:',
          description: 'Meta: update date label',
          id: 'tLC7bh',
        });
      case 'website':
        return intl.formatMessage({
          defaultMessage: 'Official website:',
          description: 'Meta: official website label',
          id: 'GRyyfy',
        });
      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();
    const dateString = 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}`),
      }
    );

    return target ? (
      <Link href={target}>
        <time dateTime={isoDateTime}>{dateString}</time>
      </Link>
    ) : (
      <time dateTime={isoDateTime}>{dateString}</time>
    );
  };

  /**
   * Retrieve the formatted comments count.
   *
   * @param comments - The comments object.
   * @returns {string | JSX.Element} - The comments count.
   */
  const getCommentsCount = (comments: MetaComments): string | JSX.Element => {
    const { about, count, target } = comments;
    const commentsCount = intl.formatMessage(
      {
        defaultMessage:
          '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>',
        description: 'Meta: comments count',
        id: '02rgLO',
      },
      {
        a11y: (chunks: ReactNode) => (
          <span className="screen-reader-text">{chunks}</span>
        ),
        commentsCount: count,
        title: about,
      }
    );

    return target ? (
      <Link href={target}>{commentsCount as JSX.Element}</Link>
    ) : (
      (commentsCount as JSX.Element)
    );
  };

  /**
   * 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 MetaKey>(
    key: T,
    value: MetaData[T]
  ): string | ReactNode | ReactNode[] => {
    switch (key) {
      case 'comments':
        return getCommentsCount(value as MetaComments);
      case 'creation':
      case 'publication':
      case 'update':
        return getDate(value as MetaDate);
      case 'total':
        return intl.formatMessage(
          {
            defaultMessage:
              '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
            description: 'BlogPage: posts count meta',
            id: 'OF5cPz',
          },
          { postsCount: value as number }
        );
      case 'website':
        const url = value as string;
        return (
          <Link href={url} external={true}>
            {url}
          </Link>
        );
      default:
        return value as string | ReactNode | ReactNode[];
    }
  };

  /**
   * Transform the metadata to description list item format.
   *
   * @param {MetaData} items - The meta.
   * @returns {DescriptionListItem[]} The formatted description list items.
   */
  const getItems = (items: MetaData): DescriptionListItem[] => {
    const listItems: DescriptionListItem[] = Object.entries(items)
      .map(([key, value]) => {
        if (!key || !value) return;

        const metaKey = key as MetaKey;

        return {
          id: metaKey,
          label:
            metaKey === 'custom'
              ? (value as CustomMeta).label
              : getLabel(metaKey),
          layout: itemsLayout,
          value:
            metaKey === 'custom' && (value as CustomMeta)
              ? (value as CustomMeta).value
              : getValue(metaKey, value),
        } as DescriptionListItem;
      })
      .filter((item): item is DescriptionListItem => !!item);

    return listItems;
  };

  return (
    <DescriptionList
      items={getItems(data)}
      withSeparator={withSeparator}
      {...props}
    />
  );
};

export default Meta;