From c9c1c90b30e243563bb4f731da15b3fe657556d2 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 6 Nov 2023 18:08:04 +0100 Subject: refactor(components): replace Summary component with PostPreview * rename component to PostPreview because Summary is an HTML element and it could lead to confusion * replace `title` and `titleLevel` with `heading` and `headingLvl` because `title` is a native attribute * rename `intro` prop to `excerpt` * extract `cover` from `meta` prop * rewrite meta type * extract meta logic into a new component --- src/components/molecules/card/card.module.scss | 12 +- src/components/organisms/index.ts | 1 + src/components/organisms/layout/index.ts | 1 - .../organisms/layout/posts-list.fixture.ts | 61 ----- .../organisms/layout/posts-list.fixture.tsx | 62 +++++ src/components/organisms/layout/posts-list.tsx | 32 ++- src/components/organisms/layout/summary.fixture.ts | 25 -- .../organisms/layout/summary.module.scss | 96 -------- .../organisms/layout/summary.stories.tsx | 107 --------- src/components/organisms/layout/summary.test.tsx | 55 ----- src/components/organisms/layout/summary.tsx | 235 ------------------- src/components/organisms/post-preview/index.ts | 2 + .../post-preview/post-preview-meta/index.ts | 1 + .../post-preview-meta/post-preview-meta.test.tsx | 141 ++++++++++++ .../post-preview-meta/post-preview-meta.tsx | 254 +++++++++++++++++++++ .../post-preview/post-preview.module.scss | 96 ++++++++ .../post-preview/post-preview.stories.tsx | 157 +++++++++++++ .../organisms/post-preview/post-preview.test.tsx | 66 ++++++ .../organisms/post-preview/post-preview.tsx | 132 +++++++++++ src/i18n/en.json | 82 +++---- src/i18n/fr.json | 82 +++---- src/utils/helpers/index.ts | 1 + src/utils/helpers/pages.ts | 102 --------- src/utils/helpers/pages.tsx | 124 ++++++++++ src/utils/helpers/reading-time.test.ts | 31 +++ src/utils/helpers/reading-time.ts | 46 ++++ 26 files changed, 1231 insertions(+), 773 deletions(-) delete mode 100644 src/components/organisms/layout/posts-list.fixture.ts create mode 100644 src/components/organisms/layout/posts-list.fixture.tsx delete mode 100644 src/components/organisms/layout/summary.fixture.ts delete mode 100644 src/components/organisms/layout/summary.module.scss delete mode 100644 src/components/organisms/layout/summary.stories.tsx delete mode 100644 src/components/organisms/layout/summary.test.tsx delete mode 100644 src/components/organisms/layout/summary.tsx create mode 100644 src/components/organisms/post-preview/index.ts create mode 100644 src/components/organisms/post-preview/post-preview-meta/index.ts create mode 100644 src/components/organisms/post-preview/post-preview-meta/post-preview-meta.test.tsx create mode 100644 src/components/organisms/post-preview/post-preview-meta/post-preview-meta.tsx create mode 100644 src/components/organisms/post-preview/post-preview.module.scss create mode 100644 src/components/organisms/post-preview/post-preview.stories.tsx create mode 100644 src/components/organisms/post-preview/post-preview.test.tsx create mode 100644 src/components/organisms/post-preview/post-preview.tsx delete mode 100644 src/utils/helpers/pages.ts create mode 100644 src/utils/helpers/pages.tsx create mode 100644 src/utils/helpers/reading-time.test.ts create mode 100644 src/utils/helpers/reading-time.ts (limited to 'src') diff --git a/src/components/molecules/card/card.module.scss b/src/components/molecules/card/card.module.scss index 65f92f6..d87114b 100644 --- a/src/components/molecules/card/card.module.scss +++ b/src/components/molecules/card/card.module.scss @@ -232,9 +232,15 @@ $breakpoint: 50ch; grid-column: 3; } - :where(.footer) .meta { - grid-row-start: 1; - flex-flow: column wrap; + :where(.footer) { + .actions { + padding-bottom: 0; + } + + .meta { + grid-row-start: 1; + flex-flow: column wrap; + } } :where(.body:first-child + .footer) .meta, diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 092b78e..43414fa 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -2,4 +2,5 @@ export * from './forms'; export * from './layout'; export * from './nav'; export * from './navbar'; +export * from './post-preview'; export * from './widgets'; diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts index 86670e3..552ed27 100644 --- a/src/components/organisms/layout/index.ts +++ b/src/components/organisms/layout/index.ts @@ -3,4 +3,3 @@ export * from './comments-list'; export * from './no-results'; export * from './overview'; export * from './posts-list'; -export * from './summary'; diff --git a/src/components/organisms/layout/posts-list.fixture.ts b/src/components/organisms/layout/posts-list.fixture.ts deleted file mode 100644 index dfb0d97..0000000 --- a/src/components/organisms/layout/posts-list.fixture.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Post } from './posts-list'; - -export const introPost1 = - 'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.'; - -export const introPost2 = - 'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.'; - -export const introPost3 = - 'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.'; - -export const cover = { - alt: 'cover', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, -}; - -export const posts: Post[] = [ - { - intro: introPost1, - id: 'post-1', - meta: { - cover, - dates: { publication: '2022-02-26' }, - wordsCount: introPost1.split(' ').length, - thematics: [ - { id: 1, name: 'Cat 1', url: '#' }, - { id: 2, name: 'Cat 2', url: '#' }, - ], - commentsCount: 1, - }, - title: 'Ratione velit fuga', - url: '#', - }, - { - intro: introPost2, - id: 'post-2', - meta: { - dates: { publication: '2022-02-20' }, - wordsCount: introPost2.split(' ').length, - thematics: [{ id: 2, name: 'Cat 2', url: '#' }], - commentsCount: 0, - }, - title: 'Debitis laudantium laudantium', - url: '#', - }, - { - intro: introPost3, - id: 'post-3', - meta: { - cover, - dates: { publication: '2021-12-20' }, - wordsCount: introPost3.split(' ').length, - thematics: [{ id: 1, name: 'Cat 1', url: '#' }], - commentsCount: 3, - }, - title: 'Quaerat ut corporis', - url: '#', - }, -]; diff --git a/src/components/organisms/layout/posts-list.fixture.tsx b/src/components/organisms/layout/posts-list.fixture.tsx new file mode 100644 index 0000000..e1f7a95 --- /dev/null +++ b/src/components/organisms/layout/posts-list.fixture.tsx @@ -0,0 +1,62 @@ +import NextImage from 'next/image'; +import type { PostData } from './posts-list'; + +export const introPost1 = + 'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.'; + +export const introPost2 = + 'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.'; + +export const introPost3 = + 'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.'; + +export const cover = { + alt: 'cover', + height: 480, + src: 'http://picsum.photos/640/480', + width: 640, +}; + +export const posts: PostData[] = [ + { + cover: , + excerpt: introPost1, + id: 'post-1', + meta: { + publicationDate: '2022-02-26', + wordsCount: introPost1.split(' ').length, + thematics: [ + { id: 1, name: 'Cat 1', url: '#' }, + { id: 2, name: 'Cat 2', url: '#' }, + ], + comments: { count: 1, postHeading: 'Ratione velit fuga' }, + }, + heading: 'Ratione velit fuga', + url: '#', + }, + { + excerpt: introPost2, + id: 'post-2', + meta: { + publicationDate: '2022-02-20', + wordsCount: introPost2.split(' ').length, + thematics: [{ id: 2, name: 'Cat 2', url: '#' }], + comments: { count: 0, postHeading: 'Debitis laudantium laudantium' }, + }, + heading: 'Debitis laudantium laudantium', + url: '#', + }, + { + cover: , + excerpt: introPost3, + id: 'post-3', + meta: { + publicationDate: '2021-12-20', + wordsCount: introPost3.split(' ').length, + thematics: [{ id: 1, name: 'Cat 1', url: '#' }], + comments: { count: 3, postHeading: 'Quaerat ut corporis' }, + }, + heading: 'Quaerat ut corporis', + url: '#', + }, +]; diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx index 36d3c87..40306a6 100644 --- a/src/components/organisms/layout/posts-list.tsx +++ b/src/components/organisms/layout/posts-list.tsx @@ -17,18 +17,30 @@ import { type RenderPaginationItemAriaLabel, type RenderPaginationLink, } from '../nav'; +import { + PostPreview, + type PostPreviewMetaData, + type PostPreviewProps, +} from '../post-preview'; import { NoResults } from './no-results'; import styles from './posts-list.module.scss'; -import { Summary, type SummaryProps } from './summary'; -export type Post = Omit & { +export type PostData = Pick< + PostPreviewProps, + 'cover' | 'excerpt' | 'heading' | 'url' +> & { /** * The post id. */ id: string | number; + /** + * The post meta. + */ + meta: PostPreviewMetaData & + Required>; }; -export type YearCollection = Record; +export type YearCollection = Record; export type PostsListProps = Pick & { /** @@ -54,7 +66,7 @@ export type PostsListProps = Pick & { /** * The posts data. */ - posts: Post[]; + posts: PostData[]; /** * Determine if the load more button should be visible. */ @@ -72,14 +84,14 @@ export type PostsListProps = Pick & { /** * Create a collection of posts sorted by year. * - * @param {Posts[]} data - A collection of posts. + * @param {PostData[]} data - A collection of posts. * @returns {YearCollection} The posts sorted by year. */ -const sortPostsByYear = (data: Post[]): YearCollection => { +const sortPostsByYear = (data: PostData[]): YearCollection => { const yearCollection: Partial = {}; data.forEach((post) => { - const postYear = new Date(post.meta.dates.publication) + const postYear = new Date(post.meta.publicationDate) .getFullYear() .toString(); yearCollection[postYear] = [...(yearCollection[postYear] ?? []), post]; @@ -116,12 +128,12 @@ export const PostsList: FC = ({ /** * Retrieve the list of posts. * - * @param {Posts[]} allPosts - A collection fo posts. + * @param {PostData[]} allPosts - A collection fo posts. * @param {HeadingLevel} [headingLevel] - The posts heading level (hn). * @returns {JSX.Element} The list of posts. */ const getList = ( - allPosts: Post[], + allPosts: PostData[], headingLevel: HeadingLevel = 2 ): JSX.Element => ( = ({ {allPosts.map(({ id, ...post }) => ( - + {id === lastPostId && ( diff --git a/src/components/organisms/layout/summary.fixture.ts b/src/components/organisms/layout/summary.fixture.ts deleted file mode 100644 index 6f90b4a..0000000 --- a/src/components/organisms/layout/summary.fixture.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { SummaryMeta } from './summary'; - -export const cover = { - alt: 'A cover', - height: 480, - src: 'https://picsum.photos/640/480', - width: 640, -}; - -export const intro = - 'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.'; - -export const meta: SummaryMeta = { - dates: { publication: '2022-04-11' }, - wordsCount: intro.split(' ').length, - thematics: [ - { id: 1, name: 'Cat 1', url: '#' }, - { id: 2, name: 'Cat 2', url: '#' }, - ], - commentsCount: 1, -}; - -export const title = 'Odio odit necessitatibus'; - -export const url = '#'; diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss deleted file mode 100644 index 6e0af6a..0000000 --- a/src/components/organisms/layout/summary.module.scss +++ /dev/null @@ -1,96 +0,0 @@ -@use "../../../styles/abstracts/placeholders"; - -.wrapper { - &:hover { - .icon { - :global { - animation: pulse 1.5s ease-in-out 0.2s infinite; - } - } - } -} - -.title { - font-size: var(--font-size-2xl); -} - -.intro { - > *:last-child { - margin-bottom: 0; - } - - :global { - a { - @extend %link; - - &[hreflang], - &.download, - &.external { - @extend %link-with-icon; - } - - &[hreflang] { - @extend %link-with-lang; - } - - &[hreflang]:not(.download, .external) { - --is-icon-hidden: ""; - } - - &.download { - @extend %download-link; - } - - &.external { - @extend %external-link; - } - - &.download, - &.external { - &:not([hreflang]) { - --is-lang-hidden: ""; - } - } - - &.external.download { - @extend %external-download-link; - } - } - } -} - -:global([data-theme="light"]) { - :local { - .intro { - :global { - a { - &.download { - @extend %light-download-link; - } - - &.external { - @extend %light-external-link; - } - } - } - } - } -} - -:global([data-theme="dark"]) { - :local { - .intro { - :global { - a { - &.download { - @extend %dark-download-link; - } - - &.external { - @extend %dark-external-link; - } - } - } - } - } -} diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx deleted file mode 100644 index fe8b704..0000000 --- a/src/components/organisms/layout/summary.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Summary } from './summary'; -import { cover, intro, meta } from './summary.fixture'; - -/** - * Summary - Storybook Meta - */ -export default { - title: 'Organisms/Layout/Summary', - component: Summary, - args: { - titleLevel: 2, - }, - argTypes: { - cover: { - description: 'The cover data.', - table: { - category: 'Options', - }, - type: { - name: 'object', - required: false, - value: {}, - }, - }, - excerpt: { - control: { - type: 'text', - }, - description: 'The page excerpt.', - type: { - name: 'string', - required: true, - }, - }, - meta: { - description: 'The page metadata.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The page title', - type: { - name: 'string', - required: true, - }, - }, - titleLevel: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The page title level (hn)', - table: { - category: 'Options', - defaultValue: { summary: 2 }, - }, - type: { - name: 'number', - required: false, - }, - }, - url: { - control: { - type: 'text', - }, - description: 'The page url.', - type: { - name: 'string', - required: true, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - -); - -/** - * Summary Stories - Default - */ -export const Default = Template.bind({}); -Default.args = { - intro, - meta, - title: 'Odio odit necessitatibus', - url: '#', -}; - -/** - * Summary Stories - With cover - */ -export const WithCover = Template.bind({}); -WithCover.args = { - intro, - meta: { ...meta, cover }, - title: 'Odio odit necessitatibus', - url: '#', -}; diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx deleted file mode 100644 index 3e58e9a..0000000 --- a/src/components/organisms/layout/summary.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Summary } from './summary'; -import { cover, intro, meta, title, url } from './summary.fixture'; - -describe('Summary', () => { - it('renders a title wrapped in a h2 element', () => { - render( - - ); - expect( - screen.getByRole('heading', { level: 2, name: title }) - ).toBeInTheDocument(); - }); - - it('renders an excerpt', () => { - render(); - expect(screen.getByText(intro)).toBeInTheDocument(); - }); - - it('renders a cover', () => { - render( - - ); - expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument(); - }); - - it('renders a link to the full post', () => { - render(); - expect(screen.getByRole('link', { name: title })).toBeInTheDocument(); - }); - - it('renders a read more link', () => { - render(); - expect( - screen.getByRole('link', { name: `Read more about ${title}` }) - ).toBeInTheDocument(); - }); - - it('renders some meta', () => { - render(); - expect(screen.getByText(meta.thematics![0].name)).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx deleted file mode 100644 index 0c95f90..0000000 --- a/src/components/organisms/layout/summary.tsx +++ /dev/null @@ -1,235 +0,0 @@ -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 { useReadingTime } from '../../../utils/hooks'; -import { ButtonLink, type HeadingLevel, Icon, Link, Time } from '../../atoms'; -import { - Card, - CardActions, - CardBody, - CardCover, - CardFooter, - CardHeader, - CardMeta, - CardTitle, - type MetaItemData, -} from '../../molecules'; -import styles from './summary.module.scss'; - -export type Cover = Pick; - -export type SummaryMeta = Pick< - MetaType<'article'>, - | 'author' - | 'commentsCount' - | 'cover' - | 'dates' - | 'thematics' - | 'topics' - | 'wordsCount' ->; - -export type SummaryProps = Pick & { - /** - * The post metadata. - */ - meta: SummaryMeta; - /** - * The heading level (hn). - */ - titleLevel?: HeadingLevel; - /** - * The post url. - */ - url: string; -}; - -/** - * Summary component - * - * Render a page summary. - */ -export const Summary: FC = ({ - intro, - meta, - title, - titleLevel = 2, - url, -}) => { - const intl = useIntl(); - const figureLabel = intl.formatMessage( - { - defaultMessage: '{title} cover', - description: 'Summary: figure (cover) accessible name', - id: 'RNVe1W', - }, - { title } - ); - const readMore = intl.formatMessage( - { - defaultMessage: 'Read more about {title}', - description: 'Summary: read more link', - id: 'Zpgv+f', - }, - { - title, - a11y: (chunks: ReactNode) => ( - // eslint-disable-next-line react/jsx-no-literals -- SR class allowed - {chunks} - ), - } - ); - const readingTime = useReadingTime(meta.wordsCount, true); - - const getMetaItems = (): MetaItemData[] => { - const summaryMeta: MetaItemData[] = [ - { - id: 'publication-date', - label: intl.formatMessage({ - defaultMessage: 'Published on:', - description: 'Summary: publication date label', - id: 'TvQ2Ee', - }), - value: