diff options
Diffstat (limited to 'src/components/molecules')
| -rw-r--r-- | src/components/molecules/layout/card.module.scss | 39 | ||||
| -rw-r--r-- | src/components/molecules/layout/card.stories.tsx | 38 | ||||
| -rw-r--r-- | src/components/molecules/layout/card.test.tsx | 13 | ||||
| -rw-r--r-- | src/components/molecules/layout/card.tsx | 16 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.module.scss | 18 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.stories.tsx | 57 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.test.tsx | 22 | ||||
| -rw-r--r-- | src/components/molecules/layout/meta.tsx | 304 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-footer.stories.tsx | 12 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-footer.tsx | 4 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-header.stories.tsx | 34 | ||||
| -rw-r--r-- | src/components/molecules/layout/page-header.tsx | 25 |
12 files changed, 450 insertions, 132 deletions
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(<Card title={title} titleLevel={2} url={url} meta={meta} />); - 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<CardProps> = ({ <div className={styles.tagline}>{tagline}</div> {meta && ( <footer className={styles.footer}> - <DescriptionList - items={meta} + <Meta + data={meta} layout="inline" className={styles.list} - groupClassName={styles.items} - termClassName={styles.term} - descriptionClassName={styles.description} + groupClassName={styles.meta__item} + labelClassName={styles.meta__label} + valueClassName={styles.meta__value} /> </footer> )} 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<typeof MetaComponent> = (args) => ( <MetaComponent {...args} /> ); -const data = { - publication: { name: 'Published on:', value: 'April 9th 2022' }, - categories: { - name: 'Categories:', - value: [ - <a key="category1" href="#"> - Category 1 - </a>, - <a key="category2" href="#"> - Category 2 - </a>, - ], - }, +const data: MetaData = { + publication: { date: '2022-04-09', time: '01:04:00' }, + thematics: [ + <a key="category1" href="#"> + Category 1 + </a>, + <a key="category2" href="#"> + Category 2 + </a>, + ], }; /** 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: [ + <a key="category1" href="#"> + Category 1 + </a>, + <a key="category2" href="#"> + Category 2 + </a>, + ], +}; + describe('Meta', () => { - it('renders a Meta component', () => { - render(<Meta data={{}} />); + it('format a date string', () => { + render(<Meta data={data} />); + 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<MetaProps> = ({ className, data, ...props }) => { +const Meta: FC<MetaProps> = ({ + 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 ? ( + <Link href={target}> + <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time> + </Link> + ) : ( + <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time> + ); + } + + const isoDateTime = new Date(`${date}T${time}`).toISOString(); + + return target ? ( + <Link href={target}> + <time dateTime={isoDateTime}> + {intl.formatMessage( + { + defaultMessage: '{date} at {time}', + description: 'Meta: publication date and time', + id: 'fcHeyC', + }, + { + date: getFormattedDate(dateTime.date), + time: getFormattedTime(`${dateTime.date}T${dateTime.time}`), + } + )} + </time> + </Link> + ) : ( + <time dateTime={isoDateTime}> + {intl.formatMessage( + { + defaultMessage: '{date} at {time}', + description: 'Meta: publication date and time', + id: 'fcHeyC', + }, + { + date: getFormattedDate(dateTime.date), + time: getFormattedTime(`${dateTime.date}T${dateTime.time}`), + } + )} + </time> + ); + }; + + /** + * Retrieve the formatted item value. + * + * @param {keyof MetaData} key - The meta key. + * @param {ValueOf<MetaData>} value - The meta value. + * @returns {string|ReactNode|ReactNode[]} - The formatted value. + */ + const getValue = <T extends keyof MetaData>( + 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<MetaProps> = ({ className, data, ...props }) => { return ( <DescriptionList items={getItems(data)} - className={`${styles.list} ${className}`} - descriptionClassName={styles.value} + withSeparator={withSeparator} {...props} /> ); 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<typeof PageFooterComponent> = (args) => ( <PageFooterComponent {...args} /> ); -const meta = { - topics: { name: 'More posts about:', value: <a href="#">Topic name</a> }, +const meta: MetaData = { + custom: { + label: 'More posts about:', + value: [ + <a key="topic-1" href="#"> + Topic name + </a>, + ], + }, }; /** 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<typeof PageHeader> = (args) => ( ); const meta = { - publication: { name: 'Published on:', value: 'April 9th 2022' }, - categories: { - name: 'Categories:', - value: [ - <a key="category1" href="#"> - Category 1 - </a>, - <a key="category2" href="#"> - Category 2 - </a>, - ], - }, + publication: { date: '2022-04-09' }, + thematics: [ + <a key="category1" href="#"> + Category 1 + </a>, + <a key="category2" href="#"> + Category 2 + </a>, + ], }; /** 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<PageHeaderProps> = ({ meta, title, }) => { + const getIntro = () => { + return typeof intro === 'string' ? ( + <div dangerouslySetInnerHTML={{ __html: intro }} /> + ) : ( + <div>{intro}</div> + ); + }; + return ( <header className={`${styles.wrapper} ${className}`}> <div className={styles.body}> <Heading level={1} className={styles.title} withMargin={false}> {title} </Heading> - {meta && <Meta data={meta} className={styles.meta} layout="inline" />} - {intro && <div>{intro}</div>} + {meta && ( + <Meta + data={meta} + className={styles.meta} + layout="column" + itemsLayout="inline" + /> + )} + {intro && getIntro()} </div> </header> ); |
