aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/layout/meta.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/layout/meta.tsx')
-rw-r--r--src/components/molecules/layout/meta.tsx391
1 files changed, 391 insertions, 0 deletions
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
new file mode 100644
index 0000000..74bd4ff
--- /dev/null
+++ b/src/components/molecules/layout/meta.tsx
@@ -0,0 +1,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;