aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-10 19:37:51 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commitc87c615b5866b8a8f361eeb0764bfdea85740e90 (patch)
treec27bda05fd96bbe3154472e170ba1abd5f9ea499 /src/components/organisms
parent15522ec9146f6f1956620355c44dea2a6a75b67c (diff)
refactor(components): replace Meta component with MetaList
It removes items complexity by allowing consumers to use any label/value association. Translations should also be defined by the consumer. Each item can now be configured separately (borders, layout...).
Diffstat (limited to 'src/components/organisms')
-rw-r--r--src/components/organisms/layout/cards-list.stories.tsx35
-rw-r--r--src/components/organisms/layout/cards-list.test.tsx39
-rw-r--r--src/components/organisms/layout/comment.tsx41
-rw-r--r--src/components/organisms/layout/overview.module.scss17
-rw-r--r--src/components/organisms/layout/overview.stories.tsx11
-rw-r--r--src/components/organisms/layout/overview.test.tsx30
-rw-r--r--src/components/organisms/layout/overview.tsx24
-rw-r--r--src/components/organisms/layout/summary.module.scss6
-rw-r--r--src/components/organisms/layout/summary.tsx175
9 files changed, 252 insertions, 126 deletions
diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx
index 1b5051f..03feee7 100644
--- a/src/components/organisms/layout/cards-list.stories.tsx
+++ b/src/components/organisms/layout/cards-list.stories.tsx
@@ -90,11 +90,21 @@ const items: CardsListItem[] = [
id: 'card-1',
cover: {
alt: 'card 1 picture',
- src: 'http://picsum.photos/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Velit', 'Ex', 'Alias'] },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'velit', value: 'Velit' },
+ { id: 'ex', value: 'Ex' },
+ { id: 'alias', value: 'Alias' },
+ ],
+ },
+ ],
tagline: 'Molestias ut error',
title: 'Et alias omnis',
url: '#',
@@ -103,11 +113,11 @@ const items: CardsListItem[] = [
id: 'card-2',
cover: {
alt: 'card 2 picture',
- src: 'http://picsum.photos/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Voluptas'] },
+ meta: [{ id: 'categories', label: 'Categories', value: 'Voluptas' }],
tagline: 'Quod vel accusamus',
title: 'Laboriosam doloremque mollitia',
url: '#',
@@ -116,13 +126,22 @@ const items: CardsListItem[] = [
id: 'card-3',
cover: {
alt: 'card 3 picture',
- src: 'http://picsum.photos/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: {
- thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
- },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'quisquam', value: 'Quisquam' },
+ { id: 'quia', value: 'Quia' },
+ { id: 'sapiente', value: 'Sapiente' },
+ { id: 'perspiciatis', value: 'Perspiciatis' },
+ ],
+ },
+ ],
tagline: 'Quo error eum',
title: 'Magni rem nulla',
url: '#',
diff --git a/src/components/organisms/layout/cards-list.test.tsx b/src/components/organisms/layout/cards-list.test.tsx
index 751a502..c9d6ae7 100644
--- a/src/components/organisms/layout/cards-list.test.tsx
+++ b/src/components/organisms/layout/cards-list.test.tsx
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
import { CardsList, type CardsListItem } from './cards-list';
const items: CardsListItem[] = [
@@ -7,11 +7,21 @@ const items: CardsListItem[] = [
id: 'card-1',
cover: {
alt: 'card 1 picture',
- src: 'http://placeimg.com/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Velit', 'Ex', 'Alias'] },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'velit', value: 'Velit' },
+ { id: 'ex', value: 'Ex' },
+ { id: 'alias', value: 'Alias' },
+ ],
+ },
+ ],
tagline: 'Molestias ut error',
title: 'Et alias omnis',
url: '#',
@@ -20,11 +30,11 @@ const items: CardsListItem[] = [
id: 'card-2',
cover: {
alt: 'card 2 picture',
- src: 'http://placeimg.com/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Voluptas'] },
+ meta: [{ id: 'categories', label: 'Categories', value: 'Voluptas' }],
tagline: 'Quod vel accusamus',
title: 'Laboriosam doloremque mollitia',
url: '#',
@@ -33,13 +43,22 @@ const items: CardsListItem[] = [
id: 'card-3',
cover: {
alt: 'card 3 picture',
- src: 'http://placeimg.com/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: {
- thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
- },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'quisquam', value: 'Quisquam' },
+ { id: 'quia', value: 'Quia' },
+ { id: 'sapiente', value: 'Sapiente' },
+ { id: 'perspiciatis', value: 'Perspiciatis' },
+ ],
+ },
+ ],
tagline: 'Quo error eum',
title: 'Magni rem nulla',
url: '#',
@@ -49,7 +68,7 @@ const items: CardsListItem[] = [
describe('CardsList', () => {
it('renders a list of cards', () => {
render(<CardsList items={items} titleLevel={2} />);
- expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(
+ expect(rtlScreen.getAllByRole('heading', { level: 2 })).toHaveLength(
items.length
);
});
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
index ca209f5..e1ea6b5 100644
--- a/src/components/organisms/layout/comment.tsx
+++ b/src/components/organisms/layout/comment.tsx
@@ -5,9 +5,10 @@ import { type FC, useCallback, useState } from 'react';
import { useIntl } from 'react-intl';
import type { Comment as CommentSchema, WithContext } from 'schema-dts';
import type { SingleComment } from '../../../types';
+import { getFormattedDate, getFormattedTime } from '../../../utils/helpers';
import { useSettings } from '../../../utils/hooks';
import { Button, Link } from '../../atoms';
-import { Meta } from '../../molecules';
+import { MetaList } from '../../molecules';
import { CommentForm, type CommentFormProps } from '../forms';
import styles from './comment.module.scss';
@@ -61,6 +62,20 @@ export const UserComment: FC<UserCommentProps> = ({
const { author, date } = meta;
const [publicationDate, publicationTime] = date.split(' ');
+ const isoDateTime = new Date(
+ `${publicationDate}T${publicationTime}`
+ ).toISOString();
+ const commentDate = intl.formatMessage(
+ {
+ defaultMessage: '{date} at {time}',
+ description: 'Comment: publication date and time',
+ id: 'Ld6yMP',
+ },
+ {
+ date: getFormattedDate(publicationDate),
+ time: getFormattedTime(`${publicationDate}T${publicationTime}`),
+ }
+ );
const buttonLabel = isReplying
? intl.formatMessage({
@@ -135,16 +150,24 @@ export const UserComment: FC<UserCommentProps> = ({
<span className={styles.author}>{author.name}</span>
)}
</header>
- <Meta
+ <MetaList
className={styles.date}
- data={{
- publication: {
- date: publicationDate,
- time: publicationTime,
- target: `#comment-${id}`,
- },
- }}
isInline
+ items={[
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Comment: publication date label',
+ id: 'soj7do',
+ }),
+ value: (
+ <Link href={`#comment-${id}`}>
+ <time dateTime={isoDateTime}>{commentDate}</time>
+ </Link>
+ ),
+ },
+ ]}
/>
<div
className={styles.body}
diff --git a/src/components/organisms/layout/overview.module.scss b/src/components/organisms/layout/overview.module.scss
index 59ce167..c1d9463 100644
--- a/src/components/organisms/layout/overview.module.scss
+++ b/src/components/organisms/layout/overview.module.scss
@@ -11,7 +11,7 @@
auto-fit,
min(calc(100vw - (var(--spacing-md) * 2)), 23ch)
);
- row-gap: var(--spacing-2xs);
+ row-gap: var(--spacing-sm);
@include mix.media("screen") {
@include mix.dimensions("md") {
@@ -21,21 +21,6 @@
);
}
}
-
- &--has-techno {
- div:last-child {
- gap: var(--spacing-2xs);
-
- dd {
- padding: 0 var(--spacing-2xs);
- border: fun.convert-px(1) solid var(--color-border-dark);
-
- &::before {
- display: none;
- }
- }
- }
- }
}
.cover {
diff --git a/src/components/organisms/layout/overview.stories.tsx b/src/components/organisms/layout/overview.stories.tsx
index 8f56d3a..562d7c4 100644
--- a/src/components/organisms/layout/overview.stories.tsx
+++ b/src/components/organisms/layout/overview.stories.tsx
@@ -1,5 +1,6 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import { Overview, type OverviewMeta } from './overview';
+import type { MetaItemData } from '../../molecules';
+import { Overview } from './overview';
/**
* Overview - Storybook Meta
@@ -54,10 +55,10 @@ const cover = {
width: 640,
};
-const meta: OverviewMeta = {
- creation: { date: '2022-05-09' },
- license: 'Dignissimos ratione veritatis',
-};
+const meta = [
+ { id: 'creation-date', label: 'Creation date', value: '2022-05-09' },
+ { id: 'license', label: 'License', value: 'Dignissimos ratione veritatis' },
+] satisfies MetaItemData[];
/**
* Overview Stories - Default
diff --git a/src/components/organisms/layout/overview.test.tsx b/src/components/organisms/layout/overview.test.tsx
index 0f2af7b..b98bd6f 100644
--- a/src/components/organisms/layout/overview.test.tsx
+++ b/src/components/organisms/layout/overview.test.tsx
@@ -1,27 +1,33 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { Overview, type OverviewMeta } from './overview';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import type { MetaItemData } from '../../molecules';
+import { Overview } from './overview';
const cover = {
alt: 'Incidunt unde quam',
height: 480,
- src: 'http://placeimg.com/640/480/cats',
+ src: 'https://picsum.photos/640/480',
width: 640,
};
-const data: OverviewMeta = {
- creation: { date: '2022-05-09' },
- license: 'Dignissimos ratione veritatis',
-};
+const meta = [
+ { id: 'creation-date', label: 'Creation date', value: '2022-05-09' },
+ { id: 'license', label: 'License', value: 'Dignissimos ratione veritatis' },
+] satisfies MetaItemData[];
describe('Overview', () => {
- it('renders some data', () => {
- render(<Overview meta={data} />);
- expect(screen.getByText(data.license!)).toBeInTheDocument();
+ it('renders some meta', () => {
+ render(<Overview meta={meta} />);
+
+ const metaLabels = meta.map((item) => item.label);
+
+ for (const label of metaLabels) {
+ expect(rtlScreen.getByText(label)).toBeInTheDocument();
+ }
});
it('renders a cover', () => {
- render(<Overview cover={cover} meta={data} />);
- expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+ render(<Overview cover={cover} meta={meta} />);
+ expect(rtlScreen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
});
});
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index 8af58ec..ede2627 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -1,19 +1,9 @@
import NextImage, { type ImageProps as NextImageProps } from 'next/image';
import type { FC } from 'react';
import { Figure } from '../../atoms';
-import { Meta, type MetaData } from '../../molecules';
+import { MetaList, type MetaItemData } from '../../molecules';
import styles from './overview.module.scss';
-export type OverviewMeta = Pick<
- MetaData,
- | 'creation'
- | 'license'
- | 'popularity'
- | 'repositories'
- | 'technologies'
- | 'update'
->;
-
export type OverviewProps = {
/**
* Set additional classnames to the overview wrapper.
@@ -26,7 +16,7 @@ export type OverviewProps = {
/**
* The overview meta.
*/
- meta: OverviewMeta;
+ meta: MetaItemData[];
};
/**
@@ -39,20 +29,16 @@ export const Overview: FC<OverviewProps> = ({
cover,
meta,
}) => {
- const { technologies, ...remainingMeta } = meta;
- const metaModifier = technologies ? styles['meta--has-techno'] : '';
+ const wrapperClass = `${styles.wrapper} ${className}`;
return (
- <div className={`${styles.wrapper} ${className}`}>
+ <div className={wrapperClass}>
{cover ? (
<Figure>
<NextImage {...cover} className={styles.cover} />
</Figure>
) : null}
- <Meta
- className={`${styles.meta} ${metaModifier}`}
- data={{ ...remainingMeta, technologies }}
- />
+ <MetaList className={styles.meta} hasInlinedValues items={meta} />
</div>
);
};
diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss
index 9dc1a69..ffc30ac 100644
--- a/src/components/organisms/layout/summary.module.scss
+++ b/src/components/organisms/layout/summary.module.scss
@@ -109,13 +109,9 @@
flex-flow: row wrap;
font-size: var(--font-size-sm);
- &__item {
- flex: 1 0 min(calc(100vw - 2 * var(--spacing-md)), 14ch);
- }
-
@include mix.media("screen") {
@include mix.dimensions("sm") {
- display: flex;
+ flex-flow: column wrap;
margin-top: 0;
}
}
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index fa3dfe5..f5c16cd 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -2,6 +2,7 @@ import NextImage, { type ImageProps as NextImageProps } from 'next/image';
import type { FC, ReactNode } from 'react';
import { useIntl } from 'react-intl';
import type { Article, Meta as MetaType } from '../../../types';
+import { getFormattedDate } from '../../../utils/helpers';
import { useReadingTime } from '../../../utils/hooks';
import {
ButtonLink,
@@ -11,7 +12,7 @@ import {
Link,
Figure,
} from '../../atoms';
-import { Meta, type MetaData } from '../../molecules';
+import { MetaList, type MetaItemData } from '../../molecules';
import styles from './summary.module.scss';
export type Cover = Pick<NextImageProps, 'alt' | 'src' | 'width' | 'height'>;
@@ -69,42 +70,134 @@ export const Summary: FC<SummaryProps> = ({
),
}
);
- const { author, commentsCount, cover, dates, thematics, topics, wordsCount } =
- meta;
- const readingTime = useReadingTime(wordsCount, true);
+ const readingTime = useReadingTime(meta.wordsCount, true);
- const getMeta = (): MetaData => {
- return {
- author: author?.name,
- publication: { date: dates.publication },
- update:
- dates.update && dates.publication !== dates.update
- ? { date: dates.update }
- : undefined,
- readingTime,
- thematics: thematics?.map((thematic) => (
- <Link key={thematic.id} href={thematic.url}>
- {thematic.name}
- </Link>
- )),
- topics: topics?.map((topic) => (
- <Link key={topic.id} href={topic.url}>
- {topic.name}
- </Link>
- )),
- comments: {
- about: title,
- count: commentsCount ?? 0,
- target: `${url}#comments`,
+ /**
+ * Retrieve a formatted date (and time).
+ *
+ * @param {string} date - A date string.
+ * @returns {JSX.Element} The formatted date wrapped in a time element.
+ */
+ const getDate = (date: string): JSX.Element => {
+ const isoDate = new Date(`${date}`).toISOString();
+
+ return <time dateTime={isoDate}>{getFormattedDate(date)}</time>;
+ };
+
+ const getMetaItems = (): MetaItemData[] => {
+ const summaryMeta: MetaItemData[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Summary: publication date label',
+ id: 'TvQ2Ee',
+ }),
+ value: getDate(meta.dates.publication),
},
- };
+ ];
+
+ if (meta.dates.update && meta.dates.update !== meta.dates.publication)
+ summaryMeta.push({
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'Summary: update date label',
+ id: 'f0Z/Po',
+ }),
+ value: getDate(meta.dates.update),
+ });
+
+ summaryMeta.push({
+ id: 'reading-time',
+ label: intl.formatMessage({
+ defaultMessage: 'Reading time:',
+ description: 'Summary: reading time label',
+ id: 'tyzdql',
+ }),
+ value: readingTime,
+ });
+
+ if (meta.author)
+ summaryMeta.push({
+ id: 'author',
+ label: intl.formatMessage({
+ defaultMessage: 'Written by:',
+ description: 'Summary: author label',
+ id: 'r/6HOI',
+ }),
+ value: meta.author.name,
+ });
+
+ if (meta.thematics)
+ summaryMeta.push({
+ id: 'thematics',
+ label: intl.formatMessage({
+ defaultMessage: 'Thematics:',
+ description: 'Summary: thematics label',
+ id: 'bk0WOp',
+ }),
+ value: meta.thematics.map((thematic) => {
+ return {
+ id: `thematic-${thematic.id}`,
+ value: <Link href={thematic.url}>{thematic.name}</Link>,
+ };
+ }),
+ });
+
+ if (meta.topics)
+ summaryMeta.push({
+ id: 'topics',
+ label: intl.formatMessage({
+ defaultMessage: 'Topics:',
+ description: 'Summary: topics label',
+ id: 'yIZ+AC',
+ }),
+ value: meta.topics.map((topic) => {
+ return {
+ id: `topic-${topic.id}`,
+ value: <Link href={topic.url}>{topic.name}</Link>,
+ };
+ }),
+ });
+
+ if (meta.commentsCount !== undefined) {
+ const commentsCount = intl.formatMessage(
+ {
+ defaultMessage:
+ '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>',
+ description: 'Summary: comments count',
+ id: 'ye/vlA',
+ },
+ {
+ a11y: (chunks: ReactNode) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ commentsCount: meta.commentsCount,
+ title,
+ }
+ );
+ summaryMeta.push({
+ id: 'comments-count',
+ label: intl.formatMessage({
+ defaultMessage: 'Comments:',
+ description: 'Summary: comments label',
+ id: 'bfPp0g',
+ }),
+ value: (
+ <Link href={`${url}#comments`}>{commentsCount as JSX.Element}</Link>
+ ),
+ });
+ }
+
+ return summaryMeta;
};
return (
<article className={styles.wrapper}>
- {cover ? (
+ {meta.cover ? (
<Figure>
- <NextImage {...cover} className={styles.cover} />
+ <NextImage {...meta.cover} className={styles.cover} />
</Figure>
) : null}
<header className={styles.header}>
@@ -121,21 +214,19 @@ export const Summary: FC<SummaryProps> = ({
dangerouslySetInnerHTML={{ __html: intro }}
/>
<ButtonLink className={styles['read-more']} to={url}>
- <>
- {readMore}
- <Icon
- aria-hidden={true}
- className={styles.icon}
- // eslint-disable-next-line react/jsx-no-literals -- Direction allowed
- orientation="right"
- // eslint-disable-next-line react/jsx-no-literals -- Shape allowed
- shape="arrow"
- />
- </>
+ {readMore}
+ <Icon
+ aria-hidden={true}
+ className={styles.icon}
+ // eslint-disable-next-line react/jsx-no-literals -- Direction allowed
+ orientation="right"
+ // eslint-disable-next-line react/jsx-no-literals -- Shape allowed
+ shape="arrow"
+ />
</ButtonLink>
</div>
<footer className={styles.footer}>
- <Meta className={styles.meta} data={getMeta()} spacing="xs" />
+ <MetaList className={styles.meta} items={getMetaItems()} />
</footer>
</article>
);