diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-12 17:24:13 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 00f147a7a687d5772bcc538bc606cfff972178cd (patch) | |
| tree | 27eabeb83c05e14162c51b69d4a6f36d461947fc /src/components | |
| parent | c87c615b5866b8a8f361eeb0764bfdea85740e90 (diff) | |
feat(components): add a Time component
Instead of using helpers functions to format the date each time we need
to use a time element, it makes more sense to create a new component
dedicated to this task.
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/atoms/layout/copyright.tsx | 39 | ||||
| -rw-r--r-- | src/components/atoms/layout/index.ts | 1 | ||||
| -rw-r--r-- | src/components/atoms/layout/time/index.ts | 1 | ||||
| -rw-r--r-- | src/components/atoms/layout/time/time.stories.tsx | 32 | ||||
| -rw-r--r-- | src/components/atoms/layout/time/time.test.tsx | 39 | ||||
| -rw-r--r-- | src/components/atoms/layout/time/time.tsx | 136 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.fixture.ts | 4 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.test.tsx | 10 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.tsx | 20 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.tsx | 18 |
10 files changed, 232 insertions, 68 deletions
diff --git a/src/components/atoms/layout/copyright.tsx b/src/components/atoms/layout/copyright.tsx index c60ff8b..3d56059 100644 --- a/src/components/atoms/layout/copyright.tsx +++ b/src/components/atoms/layout/copyright.tsx @@ -1,5 +1,6 @@ import type { FC, ReactNode } from 'react'; import styles from './copyright.module.scss'; +import { Time } from './time'; export type CopyrightDates = { /** @@ -32,26 +33,18 @@ export type CopyrightProps = { * * Renders a copyright information (owner, dates, license icon). */ -export const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => { - const getFormattedDate = (date: string) => { - const datetime = new Date(date).toISOString(); - - return <time dateTime={datetime}>{date}</time>; - }; - - return ( - <div className={styles.wrapper}> - <span className={styles.owner}>{owner}</span> - {icon} - {getFormattedDate(dates.start)} - {dates.end ? ( - <> - <span>-</span> - {getFormattedDate(dates.end)} - </> - ) : ( - '' - )} - </div> - ); -}; +export const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => ( + <div className={styles.wrapper}> + <span className={styles.owner}>{owner}</span> + {icon} + <Time date={dates.start} hideDay hideMonth /> + {dates.end ? ( + <> + <span>-</span> + <Time date={dates.end} hideDay hideMonth /> + </> + ) : ( + '' + )} + </div> +); diff --git a/src/components/atoms/layout/index.ts b/src/components/atoms/layout/index.ts index 3f2f8dc..c37ff02 100644 --- a/src/components/atoms/layout/index.ts +++ b/src/components/atoms/layout/index.ts @@ -6,3 +6,4 @@ export * from './header'; export * from './main'; export * from './nav'; export * from './section'; +export * from './time'; diff --git a/src/components/atoms/layout/time/index.ts b/src/components/atoms/layout/time/index.ts new file mode 100644 index 0000000..47e4e1f --- /dev/null +++ b/src/components/atoms/layout/time/index.ts @@ -0,0 +1 @@ +export * from './time'; diff --git a/src/components/atoms/layout/time/time.stories.tsx b/src/components/atoms/layout/time/time.stories.tsx new file mode 100644 index 0000000..d534f14 --- /dev/null +++ b/src/components/atoms/layout/time/time.stories.tsx @@ -0,0 +1,32 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Time } from './time'; + +/** + * Time - Storybook Meta + */ +export default { + title: 'Atoms/Layout/Time', + component: Time, + argTypes: { + date: { + control: { + type: 'text', + }, + description: 'A valid date string.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof Time>; + +const Template: ComponentStory<typeof Time> = (args) => <Time {...args} />; + +/** + * Time Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + date: '2022-03-15 10:44:20', +}; diff --git a/src/components/atoms/layout/time/time.test.tsx b/src/components/atoms/layout/time/time.test.tsx new file mode 100644 index 0000000..910285d --- /dev/null +++ b/src/components/atoms/layout/time/time.test.tsx @@ -0,0 +1,39 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '../../../../../tests/utils'; +import { settings } from '../../../../utils/config'; +import { Time } from './time'; + +describe('Time', () => { + it('renders a date wrapped in a time element', () => { + const date = '2022'; + + render(<Time date={date} />); + + expect(rtlScreen.getByText(new RegExp(date))).toHaveAttribute( + 'datetime', + new Date(date).toISOString() + ); + }); + + it('can show the time in addition to the date', () => { + const date = '2022'; + + render(<Time date={date} showTime />); + + expect(rtlScreen.getByText(new RegExp(date))).toHaveTextContent(/\sat\s/); + }); + + it('can show the week day in front of the date', () => { + const date = new Date(); + + render(<Time date={date.toDateString()} showWeekDay />); + + expect( + rtlScreen.getByText(new RegExp(`${date.getFullYear()}`)) + ).toHaveTextContent( + new Intl.DateTimeFormat(settings.locales.defaultLocale, { + weekday: 'long', + }).format(date) + ); + }); +}); diff --git a/src/components/atoms/layout/time/time.tsx b/src/components/atoms/layout/time/time.tsx new file mode 100644 index 0000000..02b4763 --- /dev/null +++ b/src/components/atoms/layout/time/time.tsx @@ -0,0 +1,136 @@ +import { + type ForwardRefRenderFunction, + type TimeHTMLAttributes, + forwardRef, +} from 'react'; +import { useIntl } from 'react-intl'; +import { settings } from '../../../../utils/config'; + +type GetDateOptionsConfig = { + hasDay: boolean; + hasMonth: boolean; + hasWeekDay: boolean; + hasYear: boolean; +}; + +const getDateOptions = ({ + hasDay, + hasMonth, + hasWeekDay, + hasYear, +}: GetDateOptionsConfig): Intl.DateTimeFormatOptions => { + const day: Intl.DateTimeFormatOptions['day'] = 'numeric'; + const month: Intl.DateTimeFormatOptions['month'] = 'long'; + const weekDay: Intl.DateTimeFormatOptions['weekday'] = 'long'; + const year: Intl.DateTimeFormatOptions['year'] = 'numeric'; + const options: [ + keyof Intl.DateTimeFormatOptions, + Intl.DateTimeFormatOptions[keyof Intl.DateTimeFormatOptions], + ][] = []; + + if (hasDay) options.push(['day', day]); + if (hasMonth) options.push(['month', month]); + if (hasWeekDay) options.push(['weekday', weekDay]); + if (hasYear) options.push(['year', year]); + + return Object.fromEntries(options); +}; + +export type TimeProps = Omit< + TimeHTMLAttributes<HTMLTimeElement>, + 'children' | 'dateTime' +> & { + /** + * A valid date string. + */ + date: string; + /** + * Should we hide the day number? + * + * @default false + */ + hideDay?: boolean; + /** + * Should we hide the month? + * + * @default false + */ + hideMonth?: boolean; + /** + * Should we hide the year? + * + * @default false + */ + hideYear?: boolean; + /** + * The current locale. + * + * @default settings.locales.defaultLocale + */ + locale?: string; + /** + * Should we display the time in addition to the date? + * + * @default false + */ + showTime?: boolean; + /** + * Should we display the week day? + * + * @default false + */ + showWeekDay?: boolean; +}; + +const TimeWithRef: ForwardRefRenderFunction<HTMLTimeElement, TimeProps> = ( + { + date, + hideDay = false, + hideMonth = false, + hideYear = false, + locale = settings.locales.defaultLocale, + showTime = false, + showWeekDay = false, + ...props + }, + ref +) => { + const intl = useIntl(); + const dateOptions = getDateOptions({ + hasDay: !hideDay, + hasMonth: !hideMonth, + hasWeekDay: showWeekDay, + hasYear: !hideYear, + }); + const fullDate = new Date(date); + const dateTime = fullDate.toISOString(); + const readableDate = fullDate.toLocaleDateString(locale, dateOptions); + const formattedTime = fullDate.toLocaleTimeString(locale, { + hour: 'numeric', + minute: 'numeric', + }); + const readableTime = + locale === 'fr' ? formattedTime.replace(':', 'h') : formattedTime; + + return ( + <time {...props} dateTime={dateTime} ref={ref}> + {showTime + ? intl.formatMessage( + { + defaultMessage: '{date} at {time}', + description: 'Time: readable date and time', + id: '8q5PXx', + }, + { date: readableDate, time: readableTime } + ) + : readableDate} + </time> + ); +}; + +/** + * Time component. + * + * Render a date with an optional time in a `<time>` element. + */ +export const Time = forwardRef(TimeWithRef); diff --git a/src/components/organisms/layout/comment.fixture.ts b/src/components/organisms/layout/comment.fixture.ts index f626be9..bb18d22 100644 --- a/src/components/organisms/layout/comment.fixture.ts +++ b/src/components/organisms/layout/comment.fixture.ts @@ -1,4 +1,3 @@ -import { getFormattedDate, getFormattedTime } from '../../../utils/helpers'; import type { UserCommentProps } from './comment'; export const author = { @@ -36,6 +35,3 @@ export const data: UserCommentProps = { parentId: 0, saveComment, }; - -export const formattedDate = getFormattedDate(date); -export const formattedTime = getFormattedTime(date); diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx index b64f84a..0e0ea3a 100644 --- a/src/components/organisms/layout/comment.test.tsx +++ b/src/components/organisms/layout/comment.test.tsx @@ -1,13 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import { render, screen as rtlScreen } from '../../../../tests/utils'; import { UserComment } from './comment'; -import { - author, - data, - formattedDate, - formattedTime, - id, -} from './comment.fixture'; +import { author, data, id } from './comment.fixture'; describe('UserComment', () => { it('renders an avatar', () => { @@ -29,7 +23,7 @@ describe('UserComment', () => { render(<UserComment canReply={true} {...data} />); expect( rtlScreen.getByRole('link', { - name: `${formattedDate} at ${formattedTime}`, + name: /\sat\s/, }) ).toHaveAttribute('href', `#comment-${id}`); }); diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx index e1ea6b5..cb2f16f 100644 --- a/src/components/organisms/layout/comment.tsx +++ b/src/components/organisms/layout/comment.tsx @@ -5,9 +5,8 @@ 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 { Button, Link, Time } from '../../atoms'; import { MetaList } from '../../molecules'; import { CommentForm, type CommentFormProps } from '../forms'; import styles from './comment.module.scss'; @@ -61,21 +60,6 @@ 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({ @@ -163,7 +147,7 @@ export const UserComment: FC<UserCommentProps> = ({ }), value: ( <Link href={`#comment-${id}`}> - <time dateTime={isoDateTime}>{commentDate}</time> + <Time date={date} showTime /> </Link> ), }, diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx index f5c16cd..4fe7632 100644 --- a/src/components/organisms/layout/summary.tsx +++ b/src/components/organisms/layout/summary.tsx @@ -2,7 +2,6 @@ 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,6 +10,7 @@ import { Icon, Link, Figure, + Time, } from '../../atoms'; import { MetaList, type MetaItemData } from '../../molecules'; import styles from './summary.module.scss'; @@ -72,18 +72,6 @@ export const Summary: FC<SummaryProps> = ({ ); const readingTime = useReadingTime(meta.wordsCount, true); - /** - * 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[] = [ { @@ -93,7 +81,7 @@ export const Summary: FC<SummaryProps> = ({ description: 'Summary: publication date label', id: 'TvQ2Ee', }), - value: getDate(meta.dates.publication), + value: <Time date={meta.dates.publication} />, }, ]; @@ -105,7 +93,7 @@ export const Summary: FC<SummaryProps> = ({ description: 'Summary: update date label', id: 'f0Z/Po', }), - value: getDate(meta.dates.update), + value: <Time date={meta.dates.update} />, }); summaryMeta.push({ |
