From fd9831446ff87414da772b17327368cb291192e6 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Thu, 21 Apr 2022 18:36:45 +0200 Subject: chore: add a Comment component --- src/components/organisms/layout/comment.test.tsx | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/components/organisms/layout/comment.test.tsx (limited to 'src/components/organisms/layout/comment.test.tsx') diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx new file mode 100644 index 0000000..942ed0f --- /dev/null +++ b/src/components/organisms/layout/comment.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from '@test-utils'; +import { getFormattedDate, getFormattedTime } from '@utils/helpers/format'; +import Comment from './comment'; + +const author = { + avatar: 'http://placeimg.com/640/480', + name: 'Your name', + url: 'https://www.example.test/', +}; +const content = + 'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.'; +const publication = '2021-04-03 23:04:24'; +const id = 5; +const postId = 31; + +const data = { + author, + content, + id, + postId, + publication, + saveComment: () => null, +}; + +const formattedDate = getFormattedDate(publication); +const formattedTime = getFormattedTime(publication); + +describe('Comment', () => { + it('renders an avatar', () => { + render(); + expect( + screen.getByRole('img', { name: 'Your name avatar' }) + ).toBeInTheDocument(); + }); + + it('renders the author website url', () => { + render(); + expect(screen.getByRole('link', { name: author.name })).toHaveAttribute( + 'href', + author.url + ); + }); + + it('renders a permalink to the comment', () => { + render(); + expect( + screen.getByRole('link', { + name: `${formattedDate} at ${formattedTime}`, + }) + ).toHaveAttribute('href', `/#comment-${id}`); + }); + + it('renders a reply button', () => { + render(); + expect(screen.getByRole('button', { name: 'Reply' })).toBeInTheDocument(); + }); + + it('does not render a reply button', () => { + render(); + expect( + screen.queryByRole('button', { name: 'Reply' }) + ).not.toBeInTheDocument(); + }); +}); -- cgit v1.2.3 From cb6a54e54f2f013e06049b20388ca78e26201e16 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 22 Apr 2022 17:27:01 +0200 Subject: chore: add a CommentsList component --- .../organisms/layout/comment.stories.tsx | 1 - src/components/organisms/layout/comment.test.tsx | 2 - src/components/organisms/layout/comment.tsx | 4 - .../organisms/layout/comments-list.module.scss | 16 +++ .../organisms/layout/comments-list.stories.tsx | 144 +++++++++++++++++++++ .../organisms/layout/comments-list.test.tsx | 66 ++++++++++ src/components/organisms/layout/comments-list.tsx | 65 ++++++++++ 7 files changed, 291 insertions(+), 7 deletions(-) create mode 100644 src/components/organisms/layout/comments-list.module.scss create mode 100644 src/components/organisms/layout/comments-list.stories.tsx create mode 100644 src/components/organisms/layout/comments-list.test.tsx create mode 100644 src/components/organisms/layout/comments-list.tsx (limited to 'src/components/organisms/layout/comment.test.tsx') diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx index 01d9ac6..b14621b 100644 --- a/src/components/organisms/layout/comment.stories.tsx +++ b/src/components/organisms/layout/comment.stories.tsx @@ -109,7 +109,6 @@ Comment.args = { content: 'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.', id: 2, - postId: 21, publication: '2021-04-03 23:04:24', saveComment: () => null, // @ts-ignore - Needed because of the placeholder image. diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx index 942ed0f..9e537e5 100644 --- a/src/components/organisms/layout/comment.test.tsx +++ b/src/components/organisms/layout/comment.test.tsx @@ -11,13 +11,11 @@ const content = 'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.'; const publication = '2021-04-03 23:04:24'; const id = 5; -const postId = 31; const data = { author, content, id, - postId, publication, saveComment: () => null, }; diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index f4b822a..6d41c00 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -40,10 +40,6 @@ export type CommentProps = { * The comment id. */ id: number | string; - /** - * The post id. - */ - postId: number | string; /** * The comment date. */ diff --git a/src/components/organisms/layout/comments-list.module.scss b/src/components/organisms/layout/comments-list.module.scss new file mode 100644 index 0000000..803a418 --- /dev/null +++ b/src/components/organisms/layout/comments-list.module.scss @@ -0,0 +1,16 @@ +@use "@styles/abstracts/placeholders"; + +.list { + @extend %reset-ordered-list; + + & & { + margin: var(--spacing-sm) 0; + padding-left: var(--spacing-sm); + } +} + +.item { + &:not(:last-child) { + margin-bottom: var(--spacing-sm); + } +} diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx new file mode 100644 index 0000000..9edf368 --- /dev/null +++ b/src/components/organisms/layout/comments-list.stories.tsx @@ -0,0 +1,144 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import CommentsListComponent, { Comment } from './comments-list'; + +/** + * CommentsList - Storybook Meta + */ +export default { + title: 'Organisms/Layout/CommentsList', + component: CommentsListComponent, + argTypes: { + comments: { + control: { + type: null, + }, + description: 'An array of comments.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + depth: { + control: { + type: 'number', + }, + description: 'The maximum depth. Use `0` to not display nested comments.', + type: { + name: 'number', + required: true, + }, + }, + saveComment: { + control: { + type: null, + }, + description: 'A callback function to save the comment form data.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: true, + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +const comments: Comment[] = [ + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 1', + }, + content: + 'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.', + id: 1, + publication: '2021-04-03 18:04:11', + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, + }, + { + child: [ + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 4', + }, + content: + 'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.', + id: 4, + publication: '2021-04-03 23:04:24', + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, + }, + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 1', + }, + content: + 'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.', + id: 5, + publication: '2021-04-04 08:05:14', + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, + }, + ], + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 2', + url: '#', + }, + content: + 'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.', + id: 2, + publication: '2021-04-03 23:30:20', + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, + }, + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 3', + }, + content: + 'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.', + id: 3, + publication: '2021-09-13 13:24:54', + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, + }, +]; + +/** + * Layout Stories - Without child comments + */ +export const WithoutChildComments = Template.bind({}); +WithoutChildComments.args = { + comments, + depth: 0, + saveComment: () => null, +}; + +/** + * Layout Stories - With child comments + */ +export const WithChildComments = Template.bind({}); +WithChildComments.args = { + comments, + depth: 1, + saveComment: () => null, +}; diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx new file mode 100644 index 0000000..542b1df --- /dev/null +++ b/src/components/organisms/layout/comments-list.test.tsx @@ -0,0 +1,66 @@ +import { render } from '@test-utils'; +import CommentsList, { type Comment } from './comments-list'; + +const comments: Comment[] = [ + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 1', + }, + content: + 'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.', + id: 1, + publication: '2021-04-03 18:04:11', + }, + { + child: [ + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 4', + }, + content: + 'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.', + id: 4, + publication: '2021-04-03 23:04:24', + }, + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 1', + }, + content: + 'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.', + id: 5, + publication: '2021-04-04 08:05:14', + }, + ], + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 2', + url: '#', + }, + content: + 'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.', + id: 2, + publication: '2021-04-03 23:30:20', + }, + { + author: { + avatar: 'http://placeimg.com/640/480', + name: 'Author 3', + }, + content: + 'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.', + id: 3, + publication: '2021-05-13 13:24:54', + }, +]; + +describe('CommentsList', () => { + it('renders a comments list', () => { + render( + null} /> + ); + }); +}); diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx new file mode 100644 index 0000000..03f508e --- /dev/null +++ b/src/components/organisms/layout/comments-list.tsx @@ -0,0 +1,65 @@ +import SingleComment, { + type CommentProps, +} from '@components/organisms/layout/comment'; +import { FC } from 'react'; +import styles from './comments-list.module.scss'; + +export type Comment = Omit & { + child?: Comment[]; +}; + +export type CommentsListProps = { + /** + * An array of comments. + */ + comments: Comment[]; + /** + * The maximum depth. Use `0` to not display nested comments. + */ + depth: 0 | 1 | 2 | 3 | 4; + /** + * A callback function to save comment form data. + */ + saveComment: CommentProps['saveComment']; +}; + +/** + * CommentsList component + * + * Render a comments list. + */ +const CommentsList: FC = ({ + comments, + depth, + saveComment, +}) => { + /** + * Get each comment wrapped in a list item. + * + * @param {Comment[]} commentsList - An array of comments. + * @returns {JSX.Element[]} The list items. + */ + const getItems = ( + commentsList: Comment[], + startLevel: number + ): JSX.Element[] => { + const isLastLevel = startLevel === depth; + + return commentsList.map(({ child, ...comment }) => ( +
  • + + {child && !isLastLevel && ( +
      {getItems(child, startLevel + 1)}
    + )} +
  • + )); + }; + + return
      {getItems(comments, 0)}
    ; +}; + +export default CommentsList; -- cgit v1.2.3 From 0d59a6d2995b4119865271ed1908ede0bb96497c Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 9 May 2022 18:19:38 +0200 Subject: refactor: rewrite DescriptionList and Meta components The meta can have different layout. The previous implementation was not enough to easily change the layout. Also, I prefer to restrict the meta types and it prevents me to repeat myself for the labels. --- .storybook/preview.js | 9 + .../atoms/lists/description-list-item.module.scss | 38 +++ .../atoms/lists/description-list-item.stories.tsx | 132 +++++++++ .../atoms/lists/description-list-item.test.tsx | 17 ++ .../atoms/lists/description-list-item.tsx | 73 +++++ .../atoms/lists/description-list.module.scss | 49 +--- .../atoms/lists/description-list.stories.tsx | 80 +++++- .../atoms/lists/description-list.test.tsx | 8 +- src/components/atoms/lists/description-list.tsx | 71 ++--- src/components/molecules/layout/card.module.scss | 39 ++- src/components/molecules/layout/card.stories.tsx | 38 ++- src/components/molecules/layout/card.test.tsx | 13 +- src/components/molecules/layout/card.tsx | 16 +- src/components/molecules/layout/meta.module.scss | 18 -- src/components/molecules/layout/meta.stories.tsx | 57 ++-- src/components/molecules/layout/meta.test.tsx | 22 +- src/components/molecules/layout/meta.tsx | 304 +++++++++++++++++++-- .../molecules/layout/page-footer.stories.tsx | 12 +- src/components/molecules/layout/page-footer.tsx | 4 +- .../molecules/layout/page-header.stories.tsx | 34 ++- src/components/molecules/layout/page-header.tsx | 25 +- .../organisms/layout/cards-list.stories.tsx | 16 +- .../organisms/layout/cards-list.test.tsx | 26 +- .../organisms/layout/comment.module.scss | 12 +- src/components/organisms/layout/comment.test.tsx | 2 +- src/components/organisms/layout/comment.tsx | 83 +++--- .../organisms/layout/overview.module.scss | 34 ++- .../organisms/layout/overview.stories.tsx | 29 +- src/components/organisms/layout/overview.test.tsx | 19 +- src/components/organisms/layout/overview.tsx | 41 ++- .../organisms/layout/posts-list.stories.tsx | 80 ++---- .../organisms/layout/posts-list.test.tsx | 76 ++---- src/components/organisms/layout/posts-list.tsx | 2 +- .../organisms/layout/summary.module.scss | 13 + .../organisms/layout/summary.stories.tsx | 33 +-- src/components/organisms/layout/summary.test.tsx | 27 +- src/components/organisms/layout/summary.tsx | 33 +-- .../templates/page/page-layout.stories.tsx | 107 +++----- src/components/templates/page/page-layout.tsx | 14 +- src/pages/contact.tsx | 4 +- src/pages/cv.tsx | 26 +- src/pages/index.tsx | 12 +- src/pages/mentions-legales.tsx | 26 +- src/utils/helpers/dates.ts | 15 - 44 files changed, 1182 insertions(+), 607 deletions(-) create mode 100644 src/components/atoms/lists/description-list-item.module.scss create mode 100644 src/components/atoms/lists/description-list-item.stories.tsx create mode 100644 src/components/atoms/lists/description-list-item.test.tsx create mode 100644 src/components/atoms/lists/description-list-item.tsx (limited to 'src/components/organisms/layout/comment.test.tsx') diff --git a/.storybook/preview.js b/.storybook/preview.js index 4bec022..344df2a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,4 +1,5 @@ import '@styles/globals.scss'; +import { IntlProvider } from 'react-intl'; export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, @@ -9,3 +10,11 @@ export const parameters = { }, }, }; + +export const decorators = [ + (Story) => ( + + + + ), +]; diff --git a/src/components/atoms/lists/description-list-item.module.scss b/src/components/atoms/lists/description-list-item.module.scss new file mode 100644 index 0000000..60cad57 --- /dev/null +++ b/src/components/atoms/lists/description-list-item.module.scss @@ -0,0 +1,38 @@ +.term { + color: var(--color-fg-light); + font-weight: 600; +} + +.description { + margin: 0; + word-break: break-all; +} + +.wrapper { + display: flex; + width: fit-content; + + &--has-separator { + .description:not(:first-of-type) { + &::before { + content: "/\0000a0"; + } + } + } + + &--inline, + &--inline-values { + flex-flow: row wrap; + column-gap: var(--spacing-2xs); + } + + &--inline-values { + .term { + flex: 1 1 100%; + } + } + + &--stacked { + flex-flow: column wrap; + } +} diff --git a/src/components/atoms/lists/description-list-item.stories.tsx b/src/components/atoms/lists/description-list-item.stories.tsx new file mode 100644 index 0000000..e05493c --- /dev/null +++ b/src/components/atoms/lists/description-list-item.stories.tsx @@ -0,0 +1,132 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import DescriptionListItemComponent from './description-list-item'; + +export default { + title: 'Atoms/Typography/Lists/DescriptionList/Item', + component: DescriptionListItemComponent, + args: { + layout: 'stacked', + withSeparator: false, + }, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the list item wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + descriptionClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the list item description.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + label: { + control: { + type: 'text', + }, + description: 'The item label.', + type: { + name: 'string', + required: true, + }, + }, + layout: { + control: { + type: 'select', + }, + description: 'The item layout.', + options: ['inline', 'inline-values', 'stacked'], + table: { + category: 'Styles', + defaultValue: { summary: 'stacked' }, + }, + type: { + name: 'string', + required: false, + }, + }, + termClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the list item term.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + value: { + description: 'The item value.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + withSeparator: { + control: { + type: 'boolean', + }, + description: 'Add a slash as separator between multiple values.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = ( + args +) => ; + +export const SingleValueStacked = Template.bind({}); +SingleValueStacked.args = { + label: 'Recusandae vitae tenetur', + value: ['praesentium'], + layout: 'stacked', +}; + +export const SingleValueInlined = Template.bind({}); +SingleValueInlined.args = { + label: 'Recusandae vitae tenetur', + value: ['praesentium'], + layout: 'inline', +}; + +export const MultipleValuesStacked = Template.bind({}); +MultipleValuesStacked.args = { + label: 'Recusandae vitae tenetur', + value: ['praesentium', 'voluptate', 'tempore'], + layout: 'stacked', +}; + +export const MultipleValuesInlined = Template.bind({}); +MultipleValuesInlined.args = { + label: 'Recusandae vitae tenetur', + value: ['praesentium', 'voluptate', 'tempore'], + layout: 'inline-values', + withSeparator: true, +}; diff --git a/src/components/atoms/lists/description-list-item.test.tsx b/src/components/atoms/lists/description-list-item.test.tsx new file mode 100644 index 0000000..730a52f --- /dev/null +++ b/src/components/atoms/lists/description-list-item.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from '@test-utils'; +import DescriptionListItem from './description-list-item'; + +const itemLabel = 'Repellendus corporis facilis'; +const itemValue = ['quos', 'eum']; + +describe('DescriptionListItem', () => { + it('renders a couple of label', () => { + render(); + expect(screen.getByRole('term')).toHaveTextContent(itemLabel); + }); + + it('renders the right number of values', () => { + render(); + expect(screen.getAllByRole('definition')).toHaveLength(itemValue.length); + }); +}); diff --git a/src/components/atoms/lists/description-list-item.tsx b/src/components/atoms/lists/description-list-item.tsx new file mode 100644 index 0000000..9505d01 --- /dev/null +++ b/src/components/atoms/lists/description-list-item.tsx @@ -0,0 +1,73 @@ +import { FC, ReactNode, useId } from 'react'; +import styles from './description-list-item.module.scss'; + +export type ItemLayout = 'inline' | 'inline-values' | 'stacked'; + +export type DescriptionListItemProps = { + /** + * Set additional classnames to the list item wrapper. + */ + className?: string; + /** + * Set additional classnames to the list item description. + */ + descriptionClassName?: string; + /** + * The item label. + */ + label: string; + /** + * The item layout. + */ + layout?: ItemLayout; + /** + * Set additional classnames to the list item term. + */ + termClassName?: string; + /** + * The item value. + */ + value: ReactNode | ReactNode[]; + /** + * If true, use a slash to delimitate multiple values. + */ + withSeparator?: boolean; +}; + +/** + * DescriptionListItem component + * + * Render a couple of dt/dd wrapped in a div. + */ +const DescriptionListItem: FC = ({ + className = '', + descriptionClassName = '', + label, + termClassName = '', + value, + layout = 'stacked', + withSeparator = false, +}) => { + const id = useId(); + const layoutStyles = styles[`wrapper--${layout}`]; + const separatorStyles = withSeparator ? styles['wrapper--has-separator'] : ''; + const itemValues = Array.isArray(value) ? value : [value]; + + return ( +
    +
    {label}
    + {itemValues.map((currentValue, index) => ( +
    + {currentValue} +
    + ))} +
    + ); +}; + +export default DescriptionListItem; diff --git a/src/components/atoms/lists/description-list.module.scss b/src/components/atoms/lists/description-list.module.scss index caa2711..9e913d4 100644 --- a/src/components/atoms/lists/description-list.module.scss +++ b/src/components/atoms/lists/description-list.module.scss @@ -2,53 +2,16 @@ .list { display: flex; - flex-flow: column wrap; - gap: var(--spacing-2xs); + column-gap: var(--spacing-md); + row-gap: var(--spacing-2xs); margin: 0; - &__term { - flex: 0 0 max-content; - color: var(--color-fg-light); - font-weight: 600; + &--inline { + flex-flow: row wrap; + align-items: baseline; } - &__description { - flex: 0 0 auto; - margin: 0; - } - - &__item { - display: flex; - } - - &--inline &__item { - flex-flow: column wrap; - - @include mix.media("screen") { - @include mix.dimensions("xs") { - flex-flow: row wrap; - gap: var(--spacing-2xs); - - .list__description:not(:first-of-type) { - &::before { - content: "/"; - margin-right: var(--spacing-2xs); - } - } - } - } - } - - &--column#{&}--responsive { - @include mix.media("screen") { - @include mix.dimensions("xs") { - flex-flow: row wrap; - gap: var(--spacing-lg); - } - } - } - - &--column &__item { + &--column { flex-flow: column wrap; } } diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx index 43ee66e..347fd78 100644 --- a/src/components/atoms/lists/description-list.stories.tsx +++ b/src/components/atoms/lists/description-list.stories.tsx @@ -1,16 +1,15 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; -import DescriptionListComponent, { - DescriptionListItem, -} from './description-list'; +import DescriptionList, { DescriptionListItem } from './description-list'; /** * DescriptionList - Storybook Meta */ export default { - title: 'Atoms/Typography/Lists', - component: DescriptionListComponent, + title: 'Atoms/Typography/Lists/DescriptionList', + component: DescriptionList, args: { layout: 'column', + withSeparator: false, }, argTypes: { className: { @@ -26,6 +25,19 @@ export default { required: false, }, }, + groupClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the item wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, items: { control: { type: null, @@ -37,6 +49,19 @@ export default { value: {}, }, }, + labelClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the label wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, layout: { control: { type: 'select', @@ -52,28 +77,55 @@ export default { required: false, }, }, + valueClassName: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the value wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + withSeparator: { + control: { + type: 'boolean', + }, + description: 'Add a slash as separator between multiple values.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, }, -} as ComponentMeta; +} as ComponentMeta; -const Template: ComponentStory = (args) => ( - +const Template: ComponentStory = (args) => ( + ); const items: DescriptionListItem[] = [ - { id: 'term-1', term: 'Term 1:', value: ['Value for term 1'] }, - { id: 'term-2', term: 'Term 2:', value: ['Value for term 2'] }, + { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] }, + { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] }, { id: 'term-3', - term: 'Term 3:', + label: 'Term 3:', value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'], }, - { id: 'term-4', term: 'Term 4:', value: ['Value for term 4'] }, + { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] }, ]; /** * List Stories - Description list */ -export const DescriptionList = Template.bind({}); -DescriptionList.args = { +export const List = Template.bind({}); +List.args = { items, }; diff --git a/src/components/atoms/lists/description-list.test.tsx b/src/components/atoms/lists/description-list.test.tsx index d3f7045..83e405f 100644 --- a/src/components/atoms/lists/description-list.test.tsx +++ b/src/components/atoms/lists/description-list.test.tsx @@ -2,14 +2,14 @@ import { render } from '@test-utils'; import DescriptionList, { DescriptionListItem } from './description-list'; const items: DescriptionListItem[] = [ - { id: 'term-1', term: 'Term 1:', value: ['Value for term 1'] }, - { id: 'term-2', term: 'Term 2:', value: ['Value for term 2'] }, + { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] }, + { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] }, { id: 'term-3', - term: 'Term 3:', + label: 'Term 3:', value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'], }, - { id: 'term-4', term: 'Term 4:', value: ['Value for term 4'] }, + { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] }, ]; describe('DescriptionList', () => { diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx index a60a6a1..a8e2d53 100644 --- a/src/components/atoms/lists/description-list.tsx +++ b/src/components/atoms/lists/description-list.tsx @@ -1,4 +1,7 @@ import { FC } from 'react'; +import DescriptionListItem, { + type DescriptionListItemProps, +} from './description-list-item'; import styles from './description-list.module.scss'; export type DescriptionListItem = { @@ -7,13 +10,17 @@ export type DescriptionListItem = { */ id: string; /** - * A list term. + * The list item layout. */ - term: string; + layout?: DescriptionListItemProps['layout']; /** - * An array of values for the list term. + * A list label. */ - value: any[]; + label: DescriptionListItemProps['label']; + /** + * An array of values for the list item. + */ + value: DescriptionListItemProps['value']; }; export type DescriptionListProps = { @@ -21,10 +28,6 @@ export type DescriptionListProps = { * Set additional classnames to the list wrapper. */ className?: string; - /** - * Set additional classnames to the `dd` element. - */ - descriptionClassName?: string; /** * Set additional classnames to the `dt`/`dd` couple wrapper. */ @@ -34,17 +37,21 @@ export type DescriptionListProps = { */ items: DescriptionListItem[]; /** - * The list items layout. Default: column. + * Set additional classnames to the `dt` element. + */ + labelClassName?: string; + /** + * The list layout. Default: column. */ layout?: 'inline' | 'column'; /** - * Define if the layout should automatically create rows/columns. + * Set additional classnames to the `dd` element. */ - responsiveLayout?: boolean; + valueClassName?: string; /** - * Set additional classnames to the `dt` element. + * If true, use a slash to delimitate multiple values. */ - termClassName?: string; + withSeparator?: DescriptionListItemProps['withSeparator']; }; /** @@ -54,44 +61,40 @@ export type DescriptionListProps = { */ const DescriptionList: FC = ({ className = '', - descriptionClassName = '', groupClassName = '', items, + labelClassName = '', layout = 'column', - responsiveLayout = false, - termClassName = '', + valueClassName = '', + withSeparator, }) => { const layoutModifier = `list--${layout}`; - const responsiveModifier = responsiveLayout ? 'list--responsive' : ''; /** - * Retrieve the description list items wrapped in a div element. + * Retrieve the description list items. * - * @param {DescriptionListItem[]} listItems - An array of term and description couples. + * @param {DescriptionListItem[]} listItems - An array of items. * @returns {JSX.Element[]} The description list items. */ const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => { - return listItems.map(({ id, term, value }) => { + return listItems.map(({ id, layout: itemLayout, label, value }) => { return ( -
    -
    {term}
    - {value.map((currentValue, index) => ( -
    - {currentValue} -
    - ))} -
    + ); }); }; return ( -
    +
    {getItems(items)}
    ); diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss index d5b9836..3af8f24 100644 --- a/src/components/molecules/layout/card.module.scss +++ b/src/components/molecules/layout/card.module.scss @@ -52,24 +52,35 @@ margin-bottom: var(--spacing-md); } - .items { - flex-flow: row wrap; - place-content: center; - gap: var(--spacing-2xs); - } + .meta { + &__item { + flex-flow: row wrap; + place-content: center; + gap: var(--spacing-2xs); + margin: auto; + } + + &__label { + flex: 0 0 100%; + } - .term { - flex: 0 0 100%; + &__value { + padding: fun.convert-px(2) var(--spacing-xs); + border: fun.convert-px(1) solid var(--color-primary-darker); + color: var(--color-fg); + font-weight: 400; + + &::before { + display: none; + } + } } - .description { - padding: fun.convert-px(2) var(--spacing-xs); - border: fun.convert-px(1) solid var(--color-primary-darker); - color: var(--color-fg); - font-weight: 400; + &:not(:disabled):focus { + text-decoration: none; - &::before { - display: none; + .title { + text-decoration: underline solid var(--color-primary) 0.3ex; } } } diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx index ed78d00..2e99bbb 100644 --- a/src/components/molecules/layout/card.stories.tsx +++ b/src/components/molecules/layout/card.stories.tsx @@ -8,6 +8,19 @@ export default { title: 'Molecules/Layout/Card', component: Card, argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the card wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, cover: { description: 'The card cover data (src, dimensions, alternative text).', table: { @@ -19,6 +32,21 @@ export default { value: {}, }, }, + coverFit: { + control: { + type: 'select', + }, + description: 'The cover fit.', + options: ['contain', 'cover', 'fill', 'scale-down'], + table: { + category: 'Options', + defaultValue: { summary: 'cover' }, + }, + type: { + name: 'string', + required: false, + }, + }, meta: { description: 'The card metadata (a publication date for example).', table: { @@ -88,13 +116,9 @@ const cover = { unoptimized: true, }; -const meta = [ - { - id: 'an-id', - term: 'Voluptates', - value: ['Autem', 'Eos'], - }, -]; +const meta = { + thematics: ['Autem', 'Eos'], +}; /** * Card Stories - Default diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx index 404bc7a..07c01e9 100644 --- a/src/components/molecules/layout/card.test.tsx +++ b/src/components/molecules/layout/card.test.tsx @@ -8,13 +8,10 @@ const cover = { width: 640, }; -const meta = [ - { - id: 'an-id', - term: 'Voluptates', - value: ['Autem', 'Eos'], - }, -]; +const meta = { + author: 'Possimus', + thematics: ['Autem', 'Eos'], +}; const tagline = 'Ut rerum incidunt'; @@ -47,6 +44,6 @@ describe('Card', () => { it('renders some meta', () => { render(); - expect(screen.getByText(meta[0].term)).toBeInTheDocument(); + expect(screen.getByText(meta.author)).toBeInTheDocument(); }); }); diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx index 89f100e..e416bd5 100644 --- a/src/components/molecules/layout/card.tsx +++ b/src/components/molecules/layout/card.tsx @@ -1,13 +1,11 @@ import ButtonLink from '@components/atoms/buttons/button-link'; import Heading, { type HeadingLevel } from '@components/atoms/headings/heading'; -import DescriptionList, { - type DescriptionListItem, -} from '@components/atoms/lists/description-list'; import { FC } from 'react'; import ResponsiveImage, { type ResponsiveImageProps, } from '../images/responsive-image'; import styles from './card.module.scss'; +import Meta, { type MetaData } from './meta'; export type Cover = { /** @@ -44,7 +42,7 @@ export type CardProps = { /** * The card meta. */ - meta?: DescriptionListItem[]; + meta?: MetaData; /** * The card tagline. */ @@ -96,13 +94,13 @@ const Card: FC = ({
    {tagline}
    {meta && (
    -
    )} diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss index 0485545..4194a6e 100644 --- a/src/components/molecules/layout/meta.module.scss +++ b/src/components/molecules/layout/meta.module.scss @@ -1,23 +1,5 @@ @use "@styles/abstracts/mixins" as mix; -.list { - display: grid; - grid-template-columns: repeat(1, minmax(0, 1fr)); - gap: var(--spacing-sm); - - @include mix.media("screen") { - @include mix.dimensions("2xs") { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - @include mix.dimensions("sm") { - display: flex; - flex-flow: column nowrap; - gap: var(--spacing-2xs); - } - } -} - .value { word-break: break-all; } diff --git a/src/components/molecules/layout/meta.stories.tsx b/src/components/molecules/layout/meta.stories.tsx index 0323f90..a1755a0 100644 --- a/src/components/molecules/layout/meta.stories.tsx +++ b/src/components/molecules/layout/meta.stories.tsx @@ -1,5 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; -import MetaComponent from './meta'; +import MetaComponent, { MetaData } from './meta'; /** * Meta - Storybook Meta @@ -8,25 +8,41 @@ export default { title: 'Molecules/Layout', component: MetaComponent, argTypes: { - className: { + data: { + description: 'The page metadata.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + itemsLayout: { control: { - type: 'text', + type: 'select', }, - description: 'Set additional classnames to the meta wrapper.', + description: 'The items layout.', + options: ['inline', 'inline-values', 'stacked'], table: { - category: 'Styles', + category: 'Options', + defaultValue: { summary: 'inline-values' }, }, type: { name: 'string', required: false, }, }, - data: { - description: 'The page metadata.', + withSeparator: { + control: { + type: 'boolean', + }, + description: 'Add a slash as separator between multiple values.', + table: { + category: 'Options', + defaultValue: { summary: true }, + }, type: { - name: 'object', + name: 'boolean', required: true, - value: {}, }, }, }, @@ -36,19 +52,16 @@ const Template: ComponentStory = (args) => ( ); -const data = { - publication: { name: 'Published on:', value: 'April 9th 2022' }, - categories: { - name: 'Categories:', - value: [ - - Category 1 - , - - Category 2 - , - ], - }, +const data: MetaData = { + publication: { date: '2022-04-09', time: '01:04:00' }, + thematics: [ + + Category 1 + , + + Category 2 + , + ], }; /** diff --git a/src/components/molecules/layout/meta.test.tsx b/src/components/molecules/layout/meta.test.tsx index a738bdb..fe66d97 100644 --- a/src/components/molecules/layout/meta.test.tsx +++ b/src/components/molecules/layout/meta.test.tsx @@ -1,8 +1,24 @@ -import { render } from '@test-utils'; +import { render, screen } from '@test-utils'; +import { getFormattedDate } from '@utils/helpers/dates'; import Meta from './meta'; +const data = { + publication: { date: '2022-04-09' }, + thematics: [ + + Category 1 + , + + Category 2 + , + ], +}; + describe('Meta', () => { - it('renders a Meta component', () => { - render(); + it('format a date string', () => { + render(); + expect( + screen.getByText(getFormattedDate(data.publication.date)) + ).toBeInTheDocument(); }); }); diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx index d05396e..1401ac4 100644 --- a/src/components/molecules/layout/meta.tsx +++ b/src/components/molecules/layout/meta.tsx @@ -1,67 +1,312 @@ +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 styles from './meta.module.scss'; +import { useIntl } from 'react-intl'; -export type MetaItem = { +export type CustomMeta = { + label: string; + value: ReactNode | ReactNode[]; +}; + +export type MetaDate = { /** - * The meta name. + * A date string. Ex: `2022-04-30`. */ - name: string; + date: string; /** - * The meta value. + * A time string. Ex: `10:25:59`. */ - value: ReactNode | ReactNode[]; -}; - -export type MetaMap = { - [key: string]: MetaItem | undefined; + time?: string; + /** + * Wrap the date with a link to the given target. + */ + target?: string; }; -export type MetaProps = { +export type MetaData = { + /** + * The author name. + */ + author?: string; + /** + * The comments count. + */ + commentsCount?: string | JSX.Element; + /** + * 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[]; /** - * Set additional classnames to the meta wrapper. + * An array of thematics. */ - className?: DescriptionListProps['className']; + thematics?: string[] | JSX.Element[]; + /** + * An array of thematics. + */ + topics?: string[] | JSX.Element[]; + /** + * A total. + */ + total?: string; + /** + * The update date. + */ + update?: MetaDate; +}; + +export type MetaProps = Omit< + DescriptionListProps, + 'items' | 'withSeparator' +> & { /** * The meta data. */ - data: MetaMap; + data: MetaData; /** - * The meta layout. + * The items layout. */ - layout?: DescriptionListProps['layout']; + itemsLayout?: DescriptionListItem['layout']; /** - * Determine if the layout should be responsive. + * If true, use a slash to delimitate multiple values. Default: true. */ - responsiveLayout?: DescriptionListProps['responsiveLayout']; + withSeparator?: DescriptionListProps['withSeparator']; }; /** * Meta component * - * Renders the page metadata. + * Renders the given metadata. */ -const Meta: FC = ({ className, data, ...props }) => { +const Meta: FC = ({ + 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:', + id: 'OI0N37', + description: 'Meta: author label', + }); + case 'commentsCount': + return intl.formatMessage({ + defaultMessage: 'Comments:', + id: 'jTVIh8', + description: 'Meta: comments label', + }); + case 'creation': + return intl.formatMessage({ + defaultMessage: 'Created on:', + id: 'b4fdYE', + description: 'Meta: creation date label', + }); + case 'license': + return intl.formatMessage({ + defaultMessage: 'License:', + id: 'AuGklx', + description: 'Meta: license label', + }); + case 'popularity': + return intl.formatMessage({ + defaultMessage: 'Popularity:', + id: 'pWTj2W', + description: 'Meta: popularity label', + }); + case 'publication': + return intl.formatMessage({ + defaultMessage: 'Published on:', + id: 'QGi5uD', + description: 'Meta: publication date label', + }); + case 'readingTime': + return intl.formatMessage({ + defaultMessage: 'Reading time:', + id: 'EbFvsM', + description: 'Meta: reading time label', + }); + case 'repositories': + return intl.formatMessage({ + defaultMessage: 'Repositories:', + id: 'DssFG1', + description: 'Meta: repositories label', + }); + case 'technologies': + return intl.formatMessage({ + defaultMessage: 'Technologies:', + id: 'ADQmDF', + description: 'Meta: technologies label', + }); + case 'thematics': + return intl.formatMessage({ + defaultMessage: 'Thematics:', + id: 'bz53Us', + description: 'Meta: thematics label', + }); + case 'topics': + return intl.formatMessage({ + defaultMessage: 'Topics:', + id: 'gJNaBD', + description: 'Meta: topics label', + }); + case 'total': + return intl.formatMessage({ + defaultMessage: 'Total:', + id: '92zgdp', + description: 'Meta: total label', + }); + case 'update': + return intl.formatMessage({ + defaultMessage: 'Updated on:', + id: 'tLC7bh', + description: 'Meta: update date label', + }); + 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 ? ( + + + + ) : ( + + ); + } + + const isoDateTime = new Date(`${date}T${time}`).toISOString(); + + return target ? ( + + + + ) : ( + + ); + }; + + /** + * Retrieve the formatted item value. + * + * @param {keyof MetaData} key - The meta key. + * @param {ValueOf} value - The meta value. + * @returns {string|ReactNode|ReactNode[]} - The formatted value. + */ + const getValue = ( + key: T, + value: MetaData[T] + ): string | ReactNode | ReactNode[] => { + if (key === 'creation' || key === 'publication' || key === 'update') { + return getDate(value as MetaDate); + } + return value as string | ReactNode | ReactNode[]; + }; + /** * Transform the metadata to description list item format. * - * @param {MetaMap} items - The meta. + * @param {MetaData} items - The meta. * @returns {DescriptionListItem[]} The formatted description list items. */ - const getItems = (items: MetaMap): DescriptionListItem[] => { + const getItems = (items: MetaData): DescriptionListItem[] => { const listItems: DescriptionListItem[] = Object.entries(items) - .map(([key, item]) => { - if (!item) return; + .map(([key, value]) => { + if (!key || !value) return; - const { name, value } = item; + const metaKey = key as keyof MetaData; return { - id: key, - term: name, - value: Array.isArray(value) ? value : [value], + id: metaKey, + label: + metaKey === 'custom' + ? (value as CustomMeta).label + : getLabel(metaKey), + layout: itemsLayout, + value: + metaKey === 'custom' + ? (value as CustomMeta).value + : getValue( + metaKey, + value as string | string[] | JSX.Element | JSX.Element[] + ), } as DescriptionListItem; }) .filter((item): item is DescriptionListItem => !!item); @@ -72,8 +317,7 @@ const Meta: FC = ({ className, data, ...props }) => { return ( ); diff --git a/src/components/molecules/layout/page-footer.stories.tsx b/src/components/molecules/layout/page-footer.stories.tsx index da0a3fa..31b7a49 100644 --- a/src/components/molecules/layout/page-footer.stories.tsx +++ b/src/components/molecules/layout/page-footer.stories.tsx @@ -1,4 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { MetaData } from './meta'; import PageFooterComponent from './page-footer'; /** @@ -39,8 +40,15 @@ const Template: ComponentStory = (args) => ( ); -const meta = { - topics: { name: 'More posts about:', value: Topic name }, +const meta: MetaData = { + custom: { + label: 'More posts about:', + value: [ + + Topic name + , + ], + }, }; /** diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx index f522482..e998b1e 100644 --- a/src/components/molecules/layout/page-footer.tsx +++ b/src/components/molecules/layout/page-footer.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import Meta, { type MetaMap } from './meta'; +import Meta, { MetaData } from './meta'; export type PageFooterProps = { /** @@ -9,7 +9,7 @@ export type PageFooterProps = { /** * The footer metadata. */ - meta?: MetaMap; + meta?: MetaData; }; /** diff --git a/src/components/molecules/layout/page-header.stories.tsx b/src/components/molecules/layout/page-header.stories.tsx index 6054845..d58f8b5 100644 --- a/src/components/molecules/layout/page-header.stories.tsx +++ b/src/components/molecules/layout/page-header.stories.tsx @@ -8,6 +8,19 @@ export default { title: 'Molecules/Layout/PageHeader', component: PageHeader, argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the header element.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, intro: { control: { type: 'text', @@ -50,18 +63,15 @@ const Template: ComponentStory = (args) => ( ); const meta = { - publication: { name: 'Published on:', value: 'April 9th 2022' }, - categories: { - name: 'Categories:', - value: [ - - Category 1 - , - - Category 2 - , - ], - }, + publication: { date: '2022-04-09' }, + thematics: [ + + Category 1 + , + + Category 2 + , + ], }; /** diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx index 1663085..9abe9af 100644 --- a/src/components/molecules/layout/page-header.tsx +++ b/src/components/molecules/layout/page-header.tsx @@ -1,7 +1,7 @@ import Heading from '@components/atoms/headings/heading'; -import styles from './page-header.module.scss'; -import Meta, { type MetaMap } from './meta'; import { FC } from 'react'; +import Meta, { type MetaData } from './meta'; +import styles from './page-header.module.scss'; export type PageHeaderProps = { /** @@ -15,7 +15,7 @@ export type PageHeaderProps = { /** * The page metadata. */ - meta?: MetaMap; + meta?: MetaData; /** * The page title. */ @@ -33,14 +33,29 @@ const PageHeader: FC = ({ meta, title, }) => { + const getIntro = () => { + return typeof intro === 'string' ? ( +
    + ) : ( +
    {intro}
    + ); + }; + return (
    {title} - {meta && } - {intro &&
    {intro}
    } + {meta && ( + + )} + {intro && getIntro()}
    ); diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx index fe0ebfd..522d068 100644 --- a/src/components/organisms/layout/cards-list.stories.tsx +++ b/src/components/organisms/layout/cards-list.stories.tsx @@ -93,9 +93,7 @@ const items: CardsListItem[] = [ // @ts-ignore - Needed because of the placeholder image. unoptimized: true, }, - meta: [ - { id: 'meta-1', term: 'Quibusdam', value: ['Velit', 'Ex', 'Alias'] }, - ], + meta: { thematics: ['Velit', 'Ex', 'Alias'] }, tagline: 'Molestias ut error', title: 'Et alias omnis', url: '#', @@ -110,7 +108,7 @@ const items: CardsListItem[] = [ // @ts-ignore - Needed because of the placeholder image. unoptimized: true, }, - meta: [{ id: 'meta-1', term: 'Est', value: ['Voluptas'] }], + meta: { thematics: ['Voluptas'] }, tagline: 'Quod vel accusamus', title: 'Laboriosam doloremque mollitia', url: '#', @@ -125,13 +123,9 @@ const items: CardsListItem[] = [ // @ts-ignore - Needed because of the placeholder image. unoptimized: true, }, - meta: [ - { - id: 'meta-1', - term: 'Omnis', - value: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'], - }, - ], + meta: { + thematics: ['Quisquam', 'Quia', 'Sapiente', '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 2df3f59..7d98844 100644 --- a/src/components/organisms/layout/cards-list.test.tsx +++ b/src/components/organisms/layout/cards-list.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@test-utils'; -import CardsList from './cards-list'; +import CardsList, { type CardsListItem } from './cards-list'; -const items = [ +const items: CardsListItem[] = [ { id: 'card-1', cover: { @@ -9,10 +9,10 @@ const items = [ src: 'http://placeimg.com/640/480', width: 640, height: 480, + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, }, - meta: [ - { id: 'meta-1', term: 'Quibusdam', value: ['Velit', 'Ex', 'Alias'] }, - ], + meta: { thematics: ['Velit', 'Ex', 'Alias'] }, tagline: 'Molestias ut error', title: 'Et alias omnis', url: '#', @@ -24,8 +24,10 @@ const items = [ src: 'http://placeimg.com/640/480', width: 640, height: 480, + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, }, - meta: [{ id: 'meta-1', term: 'Est', value: ['Voluptas'] }], + meta: { thematics: ['Voluptas'] }, tagline: 'Quod vel accusamus', title: 'Laboriosam doloremque mollitia', url: '#', @@ -37,14 +39,12 @@ const items = [ src: 'http://placeimg.com/640/480', width: 640, height: 480, + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, + }, + meta: { + thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'], }, - meta: [ - { - id: 'meta-1', - term: 'Omnis', - value: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'], - }, - ], tagline: 'Quo error eum', title: 'Magni rem nulla', url: '#', diff --git a/src/components/organisms/layout/comment.module.scss b/src/components/organisms/layout/comment.module.scss index 54201de..d2b68e1 100644 --- a/src/components/organisms/layout/comment.module.scss +++ b/src/components/organisms/layout/comment.module.scss @@ -60,16 +60,20 @@ } .date { - flex-flow: row wrap; - justify-content: center; margin: var(--spacing-sm) 0; font-size: var(--font-size-sm); - text-align: center; + + &__item { + justify-content: center; + } @include mix.media("screen") { @include mix.dimensions("sm") { - justify-content: left; margin: 0 0 var(--spacing-sm); + + &__item { + justify-content: left; + } } } } diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx index 9e537e5..4961722 100644 --- a/src/components/organisms/layout/comment.test.tsx +++ b/src/components/organisms/layout/comment.test.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@test-utils'; -import { getFormattedDate, getFormattedTime } from '@utils/helpers/format'; +import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates'; import Comment from './comment'; const author = { diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index 6d41c00..248efc2 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -1,10 +1,12 @@ import Button from '@components/atoms/buttons/button'; import Link from '@components/atoms/links/link'; -import DescriptionList from '@components/atoms/lists/description-list'; -import { getFormattedDate, getFormattedTime } from '@utils/helpers/format'; +import Meta from '@components/molecules/layout/meta'; +import useSettings from '@utils/hooks/use-settings'; import Image from 'next/image'; +import Script from 'next/script'; import { FC, useState } from 'react'; import { useIntl } from 'react-intl'; +import { type Comment as CommentSchema, type WithContext } from 'schema-dts'; import CommentForm, { type CommentFormProps } from '../forms/comment-form'; import styles from './comment.module.scss'; @@ -41,7 +43,11 @@ export type CommentProps = { */ id: number | string; /** - * The comment date. + * The comment parent id. + */ + parentId?: number | string; + /** + * The comment date and time separated with a space. */ publication: string; /** @@ -60,15 +66,14 @@ const Comment: FC = ({ canReply = true, content, id, + parentId, publication, saveComment, ...props }) => { const intl = useIntl(); const [isReplying, setIsReplying] = useState(false); - const commentDate = getFormattedDate(publication); - const commentTime = getFormattedTime(publication); - const commentDateTime = new Date(publication).toISOString(); + const [publicationDate, publicationTime] = publication.split(' '); const avatarAltText = intl.formatMessage( { @@ -89,37 +94,46 @@ const Comment: FC = ({ description: 'Comment: reply button', id: 'hzHuCc', }); - const dateLabel = intl.formatMessage({ - defaultMessage: 'Published on:', - description: 'Comment: publication date label', - id: 'soj7do', - }); - const dateValue = intl.formatMessage( - { - defaultMessage: '{date} at {time}', - description: 'Comment: publication date and time', - id: 'Ld6yMP', - }, - { - date: commentDate, - time: commentTime, - } - ); const formTitle = intl.formatMessage({ defaultMessage: 'Leave a reply', description: 'Comment: comment form title', id: '2fD5CI', }); - const dateLink = ( - - - {dateValue} - - ); + const { website } = useSettings(); + + const commentSchema: WithContext = { + '@context': 'https://schema.org', + '@id': `${website.url}/#comment-${id}`, + '@type': 'Comment', + parentItem: parentId + ? { '@id': `${website.url}/#comment-${parentId}` } + : undefined, + about: { '@type': 'Article', '@id': `${website.url}/#article` }, + author: { + '@type': 'Person', + name: author.name, + image: author.avatar, + url: author.url, + }, + creator: { + '@type': 'Person', + name: author.name, + image: author.avatar, + url: author.url, + }, + dateCreated: publication, + datePublished: publication, + text: content, + }; return ( <> +