diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-10 19:37:51 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | c87c615b5866b8a8f361eeb0764bfdea85740e90 (patch) | |
| tree | c27bda05fd96bbe3154472e170ba1abd5f9ea499 /src/components/organisms | |
| parent | 15522ec9146f6f1956620355c44dea2a6a75b67c (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.tsx | 35 | ||||
| -rw-r--r-- | src/components/organisms/layout/cards-list.test.tsx | 39 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.tsx | 41 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.module.scss | 17 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.stories.tsx | 11 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.test.tsx | 30 | ||||
| -rw-r--r-- | src/components/organisms/layout/overview.tsx | 24 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.module.scss | 6 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.tsx | 175 |
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> ); |
