aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/layout
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-12 17:24:13 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commit00f147a7a687d5772bcc538bc606cfff972178cd (patch)
tree27eabeb83c05e14162c51b69d4a6f36d461947fc /src/components/atoms/layout
parentc87c615b5866b8a8f361eeb0764bfdea85740e90 (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/atoms/layout')
-rw-r--r--src/components/atoms/layout/copyright.tsx39
-rw-r--r--src/components/atoms/layout/index.ts1
-rw-r--r--src/components/atoms/layout/time/index.ts1
-rw-r--r--src/components/atoms/layout/time/time.stories.tsx32
-rw-r--r--src/components/atoms/layout/time/time.test.tsx39
-rw-r--r--src/components/atoms/layout/time/time.tsx136
6 files changed, 225 insertions, 23 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);