aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/post-preview/post-preview-meta
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/organisms/post-preview/post-preview-meta')
-rw-r--r--src/components/organisms/post-preview/post-preview-meta/index.ts1
-rw-r--r--src/components/organisms/post-preview/post-preview-meta/post-preview-meta.test.tsx141
-rw-r--r--src/components/organisms/post-preview/post-preview-meta/post-preview-meta.tsx254
3 files changed, 396 insertions, 0 deletions
diff --git a/src/components/organisms/post-preview/post-preview-meta/index.ts b/src/components/organisms/post-preview/post-preview-meta/index.ts
new file mode 100644
index 0000000..0305db3
--- /dev/null
+++ b/src/components/organisms/post-preview/post-preview-meta/index.ts
@@ -0,0 +1 @@
+export * from './post-preview-meta';
diff --git a/src/components/organisms/post-preview/post-preview-meta/post-preview-meta.test.tsx b/src/components/organisms/post-preview/post-preview-meta/post-preview-meta.test.tsx
new file mode 100644
index 0000000..188afcc
--- /dev/null
+++ b/src/components/organisms/post-preview/post-preview-meta/post-preview-meta.test.tsx
@@ -0,0 +1,141 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '../../../../../tests/utils';
+import { getReadingTimeFrom } from '../../../../utils/helpers';
+import { PostPreviewMeta, type PostPreviewMetaData } from './post-preview-meta';
+
+describe('PostPreviewMeta', () => {
+ it('can render a meta for the author', () => {
+ const meta = {
+ author: 'Gilberto70',
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Written by:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(meta.author);
+ });
+
+ it('can render a meta for a single comment', () => {
+ const meta = {
+ comments: {
+ count: 1,
+ postHeading: 'quae commodi deserunt',
+ },
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Comments:');
+ expect(rtlScreen.getByRole('definition').textContent).toBe(
+ `${meta.comments.count} comment about ${meta.comments.postHeading}`
+ );
+ });
+
+ it('can render a meta for comments with a link', () => {
+ const meta = {
+ comments: {
+ count: 3,
+ postHeading: 'quae commodi deserunt',
+ url: '#temporibus',
+ },
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Comments:');
+ expect(rtlScreen.getByRole('definition').textContent).toBe(
+ `${meta.comments.count} comments about ${meta.comments.postHeading}`
+ );
+ expect(rtlScreen.getByRole('link')).toHaveAttribute(
+ 'href',
+ meta.comments.url
+ );
+ });
+
+ it('can render a meta for the publication date', () => {
+ const meta = {
+ publicationDate: '2002',
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Published on:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ meta.publicationDate
+ );
+ });
+
+ it('can render a meta for thematics', () => {
+ const meta = {
+ thematics: [{ id: 1, name: 'autem', url: '#est' }],
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Thematic:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ meta.thematics[0].name
+ );
+ expect(rtlScreen.getByRole('link')).toHaveAttribute(
+ 'href',
+ meta.thematics[0].url
+ );
+ });
+
+ it('can render a meta for topics', () => {
+ const meta = {
+ topics: [{ id: 1, name: 'hic', url: '#ipsa' }],
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Topic:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ meta.topics[0].name
+ );
+ expect(rtlScreen.getByRole('link')).toHaveAttribute(
+ 'href',
+ meta.topics[0].url
+ );
+ });
+
+ it('can render a meta for the update date', () => {
+ const meta = {
+ updateDate: '2020',
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Updated on:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ meta.updateDate
+ );
+ });
+
+ it('does not render a meta for the update date if it is equal to the publication date', () => {
+ const meta = {
+ publicationDate: '2020',
+ updateDate: '2020',
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ const terms = rtlScreen.getAllByRole('term');
+
+ expect(terms.length).toBe(1);
+ expect(terms[0].textContent).toBe('Published on:');
+ });
+
+ it('can render a meta for the reading time', () => {
+ const meta = {
+ wordsCount: 500,
+ } satisfies PostPreviewMetaData;
+
+ render(<PostPreviewMeta meta={meta} />);
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent('Reading time:');
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(
+ `${getReadingTimeFrom(meta.wordsCount).inMinutes()} minutes`
+ );
+ });
+});
diff --git a/src/components/organisms/post-preview/post-preview-meta/post-preview-meta.tsx b/src/components/organisms/post-preview/post-preview-meta/post-preview-meta.tsx
new file mode 100644
index 0000000..5a342da
--- /dev/null
+++ b/src/components/organisms/post-preview/post-preview-meta/post-preview-meta.tsx
@@ -0,0 +1,254 @@
+import type { FC, ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+import type { PageLink } from '../../../../types';
+import { getReadingTimeFrom } from '../../../../utils/helpers';
+import { Link, Time, VisuallyHidden } from '../../../atoms';
+import {
+ CardMeta,
+ type MetaItemData,
+ type CardMetaProps,
+} from '../../../molecules';
+
+const a11y = (chunks: ReactNode) => <VisuallyHidden>{chunks}</VisuallyHidden>;
+
+export type PostPreviewMetaComment = {
+ /**
+ * The number of comments.
+ */
+ count: number;
+ /**
+ * The post heading (used to generate an accessible label).
+ */
+ postHeading: string;
+ /**
+ * An url to the comments section.
+ */
+ url?: string;
+};
+
+export type PostPreviewMetaData = {
+ /**
+ * The author name.
+ */
+ author?: string;
+ /**
+ * The number of comments on the post and eventually an url to read them.
+ */
+ comments?: PostPreviewMetaComment;
+ /**
+ * The publication date of the post.
+ */
+ publicationDate?: string;
+ /**
+ * The thematics attached to the post.
+ */
+ thematics?: PageLink[];
+ /**
+ * The topics attached to the post.
+ */
+ topics?: PageLink[];
+ /**
+ * The last modification date of the post.
+ */
+ updateDate?: string;
+ /**
+ * The number of words in the post.
+ */
+ wordsCount?: number;
+};
+
+const validMetaKeys = [
+ 'author',
+ 'comments',
+ 'publicationDate',
+ 'thematics',
+ 'topics',
+ 'updateDate',
+ 'wordsCount',
+] satisfies (keyof PostPreviewMetaData)[];
+
+const isValidMetaKey = (key: string): key is keyof PostPreviewMetaData =>
+ (validMetaKeys as string[]).includes(key);
+
+export type PostPreviewMetaProps = Omit<CardMetaProps, 'items'> & {
+ /**
+ * The post meta.
+ */
+ meta: PostPreviewMetaData;
+};
+
+export const PostPreviewMeta: FC<PostPreviewMetaProps> = ({
+ meta,
+ ...props
+}) => {
+ const intl = useIntl();
+
+ const getAuthor = (): MetaItemData | undefined => {
+ if (!meta.author) return undefined;
+
+ return {
+ id: 'author',
+ label: intl.formatMessage({
+ defaultMessage: 'Written by:',
+ description: 'PostPreviewMeta: author label',
+ id: '2U7ixo',
+ }),
+ value: meta.author,
+ };
+ };
+
+ const getCommentsCount = (): MetaItemData | undefined => {
+ if (!meta.comments) return undefined;
+
+ const commentsLabel = intl.formatMessage<ReactNode>(
+ {
+ defaultMessage:
+ '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>',
+ description: 'PostPreviewMeta: comments count',
+ id: 'NfAn9N',
+ },
+ {
+ a11y,
+ commentsCount: meta.comments.count,
+ title: meta.comments.postHeading,
+ }
+ );
+
+ return {
+ id: 'comments',
+ label: intl.formatMessage({
+ defaultMessage: 'Comments:',
+ description: 'PostPreviewMeta: comments label',
+ id: 'FCpPCm',
+ }),
+ value: meta.comments.url ? (
+ <Link href={meta.comments.url}>{commentsLabel}</Link>
+ ) : (
+ <>{commentsLabel}</>
+ ),
+ };
+ };
+
+ const getPublicationDate = (): MetaItemData | undefined => {
+ if (!meta.publicationDate) return undefined;
+
+ return {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'PostPreviewMeta: publication date label',
+ id: '+6f4p1',
+ }),
+ value: <Time date={meta.publicationDate} />,
+ };
+ };
+
+ const getThematics = (): MetaItemData | undefined => {
+ if (!meta.thematics?.length) return undefined;
+
+ return {
+ id: 'thematics',
+ label: intl.formatMessage(
+ {
+ defaultMessage:
+ '{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}',
+ description: 'PostPreviewMeta: thematics label',
+ id: '9MTBCG',
+ },
+ { thematicsCount: meta.thematics.length }
+ ),
+ value: meta.thematics.map((thematic) => {
+ return {
+ id: `thematic-${thematic.id}`,
+ value: <Link href={thematic.url}>{thematic.name}</Link>,
+ };
+ }),
+ };
+ };
+
+ const getTopics = (): MetaItemData | undefined => {
+ if (!meta.topics?.length) return undefined;
+
+ return {
+ id: 'topics',
+ label: intl.formatMessage(
+ {
+ defaultMessage:
+ '{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}',
+ description: 'PostPreviewMeta: topics label',
+ id: 'aBQYbE',
+ },
+ { topicsCount: meta.topics.length }
+ ),
+ value: meta.topics.map((topic) => {
+ return {
+ id: `topic-${topic.id}`,
+ value: <Link href={topic.url}>{topic.name}</Link>,
+ };
+ }),
+ };
+ };
+
+ const getUpdateDate = (): MetaItemData | undefined => {
+ if (!meta.updateDate || meta.updateDate === meta.publicationDate)
+ return undefined;
+
+ return {
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'PostPreviewMeta: update date label',
+ id: 'ZmRh0V',
+ }),
+ value: <Time date={meta.updateDate} />,
+ };
+ };
+
+ const getReadingTime = (): MetaItemData | undefined => {
+ if (!meta.wordsCount) return undefined;
+
+ return {
+ id: 'reading-time',
+ label: intl.formatMessage({
+ defaultMessage: 'Reading time:',
+ description: 'PostPreviewMeta: reading time label',
+ id: 'B1lS/v',
+ }),
+ value: intl.formatMessage(
+ {
+ defaultMessage:
+ '{minutesCount, plural, =0 {Less than one minute} one {# minute} other {# minutes}}',
+ description: 'PostPreviewMeta: rounded minutes count',
+ id: 'y+13Ax',
+ },
+ { minutesCount: getReadingTimeFrom(meta.wordsCount).inMinutes() }
+ ),
+ };
+ };
+
+ const items: MetaItemData[] = Object.keys(meta)
+ .filter(isValidMetaKey)
+ .map((key): MetaItemData | undefined => {
+ switch (key) {
+ case 'author':
+ return getAuthor();
+ case 'comments':
+ return getCommentsCount();
+ case 'publicationDate':
+ return getPublicationDate();
+ case 'thematics':
+ return getThematics();
+ case 'topics':
+ return getTopics();
+ case 'updateDate':
+ return getUpdateDate();
+ case 'wordsCount':
+ return getReadingTime();
+ default:
+ throw new Error('Unsupported meta key.');
+ }
+ })
+ .filter((item): item is MetaItemData => item !== undefined);
+
+ return <CardMeta {...props} items={items} />;
+};