aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-10 19:37:51 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commitc87c615b5866b8a8f361eeb0764bfdea85740e90 (patch)
treec27bda05fd96bbe3154472e170ba1abd5f9ea499
parent15522ec9146f6f1956620355c44dea2a6a75b67c (diff)
refactor(components): replace Meta component with MetaList
It removes items complexity by allowing consumers to use any label/value association. Translations should also be defined by the consumer. Each item can now be configured separately (borders, layout...).
-rw-r--r--src/components/molecules/index.ts1
-rw-r--r--src/components/molecules/layout/card.fixture.ts19
-rw-r--r--src/components/molecules/layout/card.module.scss4
-rw-r--r--src/components/molecules/layout/card.stories.tsx31
-rw-r--r--src/components/molecules/layout/card.test.tsx46
-rw-r--r--src/components/molecules/layout/card.tsx12
-rw-r--r--src/components/molecules/layout/index.ts1
-rw-r--r--src/components/molecules/layout/meta.module.scss16
-rw-r--r--src/components/molecules/layout/meta.stories.tsx45
-rw-r--r--src/components/molecules/layout/meta.test.tsx25
-rw-r--r--src/components/molecules/layout/meta.tsx395
-rw-r--r--src/components/molecules/layout/page-footer.stories.tsx18
-rw-r--r--src/components/molecules/layout/page-footer.tsx8
-rw-r--r--src/components/molecules/layout/page-header.stories.tsx38
-rw-r--r--src/components/molecules/layout/page-header.tsx6
-rw-r--r--src/components/molecules/meta-list/index.ts2
-rw-r--r--src/components/molecules/meta-list/meta-item/index.ts1
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.module.scss62
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.stories.tsx108
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.test.tsx97
-rw-r--r--src/components/molecules/meta-list/meta-item/meta-item.tsx90
-rw-r--r--src/components/molecules/meta-list/meta-list.module.scss24
-rw-r--r--src/components/molecules/meta-list/meta-list.stories.tsx70
-rw-r--r--src/components/molecules/meta-list/meta-list.test.tsx79
-rw-r--r--src/components/molecules/meta-list/meta-list.tsx78
-rw-r--r--src/components/organisms/layout/cards-list.stories.tsx35
-rw-r--r--src/components/organisms/layout/cards-list.test.tsx39
-rw-r--r--src/components/organisms/layout/comment.tsx41
-rw-r--r--src/components/organisms/layout/overview.module.scss17
-rw-r--r--src/components/organisms/layout/overview.stories.tsx11
-rw-r--r--src/components/organisms/layout/overview.test.tsx30
-rw-r--r--src/components/organisms/layout/overview.tsx24
-rw-r--r--src/components/organisms/layout/summary.module.scss6
-rw-r--r--src/components/organisms/layout/summary.tsx175
-rw-r--r--src/components/templates/page/page-layout.stories.tsx45
-rw-r--r--src/components/templates/page/page-layout.tsx10
-rw-r--r--src/i18n/en.json202
-rw-r--r--src/i18n/fr.json202
-rw-r--r--src/pages/article/[slug].tsx127
-rw-r--r--src/pages/blog/index.tsx25
-rw-r--r--src/pages/blog/page/[number].tsx25
-rw-r--r--src/pages/cv.tsx44
-rw-r--r--src/pages/index.tsx26
-rw-r--r--src/pages/mentions-legales.tsx47
-rw-r--r--src/pages/projets/[slug].tsx168
-rw-r--r--src/pages/projets/index.tsx20
-rw-r--r--src/pages/recherche/index.tsx25
-rw-r--r--src/pages/sujet/[slug].tsx76
-rw-r--r--src/pages/thematique/[slug].tsx64
-rw-r--r--src/styles/abstracts/placeholders/_lists.scss2
-rw-r--r--src/styles/pages/article.module.scss3
51 files changed, 1855 insertions, 910 deletions
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
index 70ac3c9..cb0b7eb 100644
--- a/src/components/molecules/index.ts
+++ b/src/components/molecules/index.ts
@@ -4,5 +4,6 @@ export * from './collapsible';
export * from './forms';
export * from './images';
export * from './layout';
+export * from './meta-list';
export * from './nav';
export * from './tooltip';
diff --git a/src/components/molecules/layout/card.fixture.ts b/src/components/molecules/layout/card.fixture.ts
deleted file mode 100644
index 01fe2e9..0000000
--- a/src/components/molecules/layout/card.fixture.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export const cover = {
- alt: 'A picture',
- height: 480,
- src: 'https://picsum.photos/640/480',
- width: 640,
-};
-
-export const id = 'nam';
-
-export const meta = {
- author: 'Possimus',
- thematics: ['Autem', 'Eos'],
-};
-
-export const tagline = 'Ut rerum incidunt';
-
-export const title = 'Alias qui porro';
-
-export const url = '/an-existing-url';
diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss
index 7a06508..14a5baf 100644
--- a/src/components/molecules/layout/card.module.scss
+++ b/src/components/molecules/layout/card.module.scss
@@ -1,5 +1,9 @@
@use "../../../styles/abstracts/functions" as fun;
+.footer {
+ margin-top: auto;
+}
+
.wrapper {
--scale-up: 1.05;
--scale-down: 0.95;
diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx
index a9545d1..070c978 100644
--- a/src/components/molecules/layout/card.stories.tsx
+++ b/src/components/molecules/layout/card.stories.tsx
@@ -1,6 +1,6 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { MetaItemData } from '../meta-list';
import { Card } from './card';
-import { cover, id, meta, tagline, title, url } from './card.fixture';
/**
* Card - Storybook Meta
@@ -119,6 +119,33 @@ export default {
const Template: ComponentStory<typeof Card> = (args) => <Card {...args} />;
+const cover = {
+ alt: 'A picture',
+ height: 480,
+ src: 'https://picsum.photos/640/480',
+ width: 640,
+};
+
+const id = 'nam';
+
+const meta = [
+ { id: 'author', label: 'Author', value: 'Possimus' },
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'autem', value: 'Autem' },
+ { id: 'eos', value: 'Eos' },
+ ],
+ },
+] satisfies MetaItemData[];
+
+const tagline = 'Ut rerum incidunt';
+
+const title = 'Alias qui porro';
+
+const url = '/an-existing-url';
+
/**
* Card Stories - Default
*/
diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx
index c6498b8..b690d4c 100644
--- a/src/components/molecules/layout/card.test.tsx
+++ b/src/components/molecules/layout/card.test.tsx
@@ -1,37 +1,69 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import type { MetaItemData } from '../meta-list';
import { Card } from './card';
-import { cover, id, meta, tagline, title, url } from './card.fixture';
+
+const cover = {
+ alt: 'A picture',
+ height: 480,
+ src: 'https://picsum.photos/640/480',
+ width: 640,
+};
+
+const id = 'nam';
+
+const meta = [
+ { id: 'author', label: 'Author', value: 'Possimus' },
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'autem', value: 'Autem' },
+ { id: 'eos', value: 'Eos' },
+ ],
+ },
+] satisfies MetaItemData[];
+
+const tagline = 'Ut rerum incidunt';
+
+const title = 'Alias qui porro';
+
+const url = '/an-existing-url';
describe('Card', () => {
it('renders a title wrapped in h2 element', () => {
render(<Card id={id} title={title} titleLevel={2} url={url} />);
expect(
- screen.getByRole('heading', { level: 2, name: title })
+ rtlScreen.getByRole('heading', { level: 2, name: title })
).toBeInTheDocument();
});
it('renders a link to another page', () => {
render(<Card id={id} title={title} titleLevel={2} url={url} />);
- expect(screen.getByRole('link')).toHaveAttribute('href', url);
+ expect(rtlScreen.getByRole('link')).toHaveAttribute('href', url);
});
it('renders a cover', () => {
render(
<Card id={id} title={title} titleLevel={2} url={url} cover={cover} />
);
- expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+ expect(rtlScreen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
});
it('renders a tagline', () => {
render(
<Card id={id} title={title} titleLevel={2} url={url} tagline={tagline} />
);
- expect(screen.getByText(tagline)).toBeInTheDocument();
+ expect(rtlScreen.getByText(tagline)).toBeInTheDocument();
});
it('renders some meta', () => {
render(<Card id={id} title={title} titleLevel={2} url={url} meta={meta} />);
- expect(screen.getByText(meta.author)).toBeInTheDocument();
+
+ const metaLabels = meta.map((item) => item.label);
+
+ for (const label of metaLabels) {
+ expect(rtlScreen.getByText(label)).toBeInTheDocument();
+ }
});
});
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index c316100..d90cba2 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -1,8 +1,8 @@
import NextImage, { type ImageProps as NextImageProps } from 'next/image';
import type { FC } from 'react';
import { ButtonLink, Figure, Heading, type HeadingLevel } from '../../atoms';
+import { MetaList, type MetaItemData } from '../meta-list';
import styles from './card.module.scss';
-import { Meta, type MetaData } from './meta';
export type CardProps = {
/**
@@ -20,7 +20,7 @@ export type CardProps = {
/**
* The card meta.
*/
- meta?: MetaData;
+ meta?: MetaItemData[];
/**
* The card tagline.
*/
@@ -73,7 +73,13 @@ export const Card: FC<CardProps> = ({
{tagline ? <div className={styles.tagline}>{tagline}</div> : null}
{meta ? (
<footer className={styles.footer}>
- <Meta className={styles.list} data={meta} spacing="sm" />
+ <MetaList
+ className={styles.list}
+ hasBorderedValues={meta.length < 2}
+ hasInlinedValues={meta.length < 2}
+ isCentered
+ items={meta}
+ />
</footer>
) : null}
</article>
diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts
index e43e664..58d5442 100644
--- a/src/components/molecules/layout/index.ts
+++ b/src/components/molecules/layout/index.ts
@@ -1,6 +1,5 @@
export * from './card';
export * from './code';
export * from './columns';
-export * from './meta';
export * from './page-footer';
export * from './page-header';
diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss
deleted file mode 100644
index 26faac3..0000000
--- a/src/components/molecules/layout/meta.module.scss
+++ /dev/null
@@ -1,16 +0,0 @@
-.list {
- .description:not(:first-of-type) {
- &::before {
- display: inline;
- float: left;
- content: "/";
- margin-right: var(--itemSpacing);
- }
- }
-
- &--stack {
- .term {
- flex: 0 0 100%;
- }
- }
-}
diff --git a/src/components/molecules/layout/meta.stories.tsx b/src/components/molecules/layout/meta.stories.tsx
deleted file mode 100644
index 6faa265..0000000
--- a/src/components/molecules/layout/meta.stories.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import { Meta as MetaComponent, type MetaData } from './meta';
-
-/**
- * Meta - Storybook Meta
- */
-export default {
- title: 'Molecules/Layout',
- component: MetaComponent,
- args: {},
- argTypes: {
- data: {
- description: 'The page metadata.',
- type: {
- name: 'object',
- required: true,
- value: {},
- },
- },
- },
-} as ComponentMeta<typeof MetaComponent>;
-
-const Template: ComponentStory<typeof MetaComponent> = (args) => (
- <MetaComponent {...args} />
-);
-
-const data: MetaData = {
- publication: { date: '2022-04-09', time: '01:04:00' },
- thematics: [
- <a key="category1" href="#a">
- Category 1
- </a>,
- <a key="category2" href="#b">
- Category 2
- </a>,
- ],
-};
-
-/**
- * Layout Stories - Meta
- */
-export const Meta = Template.bind({});
-Meta.args = {
- data,
-};
diff --git a/src/components/molecules/layout/meta.test.tsx b/src/components/molecules/layout/meta.test.tsx
deleted file mode 100644
index 0635fc3..0000000
--- a/src/components/molecules/layout/meta.test.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen as rtlScreen } from '../../../../tests/utils';
-import { getFormattedDate } from '../../../utils/helpers';
-import { Meta } from './meta';
-
-const data = {
- publication: { date: '2022-04-09' },
- thematics: [
- <a key="category1" href="#a">
- Category 1
- </a>,
- <a key="category2" href="#b">
- Category 2
- </a>,
- ],
-};
-
-describe('Meta', () => {
- it('format a date string', () => {
- render(<Meta data={data} />);
- expect(
- rtlScreen.getByText(getFormattedDate(data.publication.date))
- ).toBeInTheDocument();
- });
-});
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
deleted file mode 100644
index 63909a4..0000000
--- a/src/components/molecules/layout/meta.tsx
+++ /dev/null
@@ -1,395 +0,0 @@
-import type { FC, ReactNode } from 'react';
-import { useIntl } from 'react-intl';
-import { getFormattedDate, getFormattedTime } from '../../../utils/helpers';
-import {
- DescriptionList,
- type DescriptionListProps,
- Link,
- Group,
- Term,
- Description,
-} from '../../atoms';
-import styles from './meta.module.scss';
-
-export type CustomMeta = {
- label: string;
- value: ReactNode;
-};
-
-export type MetaComments = {
- /**
- * A page title.
- */
- about: string;
- /**
- * The comments count.
- */
- count: number;
- /**
- * Wrap the comments count with a link to the given target.
- */
- target?: string;
-};
-
-export type MetaDate = {
- /**
- * A date string. Ex: `2022-04-30`.
- */
- date: string;
- /**
- * A time string. Ex: `10:25:59`.
- */
- time?: string;
- /**
- * Wrap the date with a link to the given target.
- */
- target?: string;
-};
-
-export type MetaData = {
- /**
- * The author name.
- */
- author?: string;
- /**
- * The comments count.
- */
- comments?: MetaComments;
- /**
- * 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[];
- /**
- * An array of thematics.
- */
- thematics?: string[] | JSX.Element[];
- /**
- * An array of thematics.
- */
- topics?: string[] | JSX.Element[];
- /**
- * A total number of posts.
- */
- total?: number;
- /**
- * The update date.
- */
- update?: MetaDate;
- /**
- * An url.
- */
- website?: string;
-};
-
-const isCustomMeta = (
- key: keyof MetaData,
- _value: unknown
-): _value is MetaData['custom'] => key === 'custom';
-
-export type MetaProps = Omit<DescriptionListProps, 'children'> & {
- /**
- * The meta data.
- */
- data: MetaData;
-};
-
-/**
- * Meta component
- *
- * Renders the given metadata.
- */
-export const Meta: FC<MetaProps> = ({
- className = '',
- data,
- isInline = false,
- ...props
-}) => {
- const layoutClass = styles[isInline ? 'list--inline' : 'list--stack'];
- const listClass = `${styles.list} ${layoutClass} ${className}`;
- 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:',
- description: 'Meta: author label',
- id: 'OI0N37',
- });
- case 'comments':
- return intl.formatMessage({
- defaultMessage: 'Comments:',
- description: 'Meta: comments label',
- id: 'jTVIh8',
- });
- case 'creation':
- return intl.formatMessage({
- defaultMessage: 'Created on:',
- description: 'Meta: creation date label',
- id: 'b4fdYE',
- });
- case 'license':
- return intl.formatMessage({
- defaultMessage: 'License:',
- description: 'Meta: license label',
- id: 'AuGklx',
- });
- case 'popularity':
- return intl.formatMessage({
- defaultMessage: 'Popularity:',
- description: 'Meta: popularity label',
- id: 'pWTj2W',
- });
- case 'publication':
- return intl.formatMessage({
- defaultMessage: 'Published on:',
- description: 'Meta: publication date label',
- id: 'QGi5uD',
- });
- case 'readingTime':
- return intl.formatMessage({
- defaultMessage: 'Reading time:',
- description: 'Meta: reading time label',
- id: 'EbFvsM',
- });
- case 'repositories':
- return intl.formatMessage({
- defaultMessage: 'Repositories:',
- description: 'Meta: repositories label',
- id: 'DssFG1',
- });
- case 'technologies':
- return intl.formatMessage({
- defaultMessage: 'Technologies:',
- description: 'Meta: technologies label',
- id: 'ADQmDF',
- });
- case 'thematics':
- return intl.formatMessage({
- defaultMessage: 'Thematics:',
- description: 'Meta: thematics label',
- id: 'bz53Us',
- });
- case 'topics':
- return intl.formatMessage({
- defaultMessage: 'Topics:',
- description: 'Meta: topics label',
- id: 'gJNaBD',
- });
- case 'total':
- return intl.formatMessage({
- defaultMessage: 'Total:',
- description: 'Meta: total label',
- id: '92zgdp',
- });
- case 'update':
- return intl.formatMessage({
- defaultMessage: 'Updated on:',
- description: 'Meta: update date label',
- id: 'tLC7bh',
- });
- case 'website':
- return intl.formatMessage({
- defaultMessage: 'Official website:',
- description: 'Meta: official website label',
- id: 'GRyyfy',
- });
- 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();
- const dateString = 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}`),
- }
- );
-
- return target ? (
- <Link href={target}>
- <time dateTime={isoDateTime}>{dateString}</time>
- </Link>
- ) : (
- <time dateTime={isoDateTime}>{dateString}</time>
- );
- };
-
- /**
- * Retrieve the formatted comments count.
- *
- * @param comments - The comments object.
- * @returns {string | JSX.Element} - The comments count.
- */
- const getCommentsCount = (comments: MetaComments): string | JSX.Element => {
- const { about, count, target } = comments;
- const commentsCount = intl.formatMessage(
- {
- defaultMessage:
- '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>',
- description: 'Meta: comments count',
- id: '02rgLO',
- },
- {
- a11y: (chunks: ReactNode) => (
- <span className="screen-reader-text">{chunks}</span>
- ),
- commentsCount: count,
- title: about,
- }
- );
-
- return target ? (
- <Link href={target}>{commentsCount as JSX.Element}</Link>
- ) : (
- (commentsCount as JSX.Element)
- );
- };
-
- /**
- * 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[] => {
- switch (key) {
- case 'comments':
- return getCommentsCount(value as MetaComments);
- case 'creation':
- case 'publication':
- case 'update':
- return getDate(value as MetaDate);
- case 'total':
- return intl.formatMessage(
- {
- defaultMessage:
- '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
- description: 'BlogPage: posts count meta',
- id: 'OF5cPz',
- },
- { postsCount: value as number }
- );
- case 'website':
- return typeof value === 'string' ? (
- <Link href={value} isExternal>
- {value}
- </Link>
- ) : null;
- default:
- return value as string | ReactNode | ReactNode[];
- }
- };
-
- /**
- * Transform the metadata to description list item format.
- *
- * @param {MetaData} items - The meta.
- * @returns {DescriptionListItem[]} The formatted description list items.
- */
- const getItems = (items: MetaData) => {
- const entries = Object.entries(items) as [
- keyof MetaData,
- MetaData[keyof MetaData],
- ][];
- const listItems = entries.map(([key, meta]) => {
- if (!meta) return null;
-
- return (
- <Group isInline key={key} spacing="2xs">
- <Term className={styles.term}>
- {isCustomMeta(key, meta) ? meta.label : getLabel(key)}
- </Term>
- {Array.isArray(meta) ? (
- meta.map((singleMeta, index) => (
- /* eslint-disable-next-line react/no-array-index-key -- Unsafe,
- * but also temporary. This component should be removed or
- * refactored. */
- <Description className={styles.description} key={index}>
- {isCustomMeta(key, singleMeta)
- ? singleMeta
- : getValue(key, singleMeta)}
- </Description>
- ))
- ) : (
- <Description className={styles.description}>
- {isCustomMeta(key, meta) ? meta.value : getValue(key, meta)}
- </Description>
- )}
- </Group>
- );
- });
-
- return listItems;
- };
-
- return (
- <DescriptionList {...props} className={listClass} isInline={isInline}>
- {getItems(data)}
- </DescriptionList>
- );
-};
diff --git a/src/components/molecules/layout/page-footer.stories.tsx b/src/components/molecules/layout/page-footer.stories.tsx
index 8e991a4..48c8c17 100644
--- a/src/components/molecules/layout/page-footer.stories.tsx
+++ b/src/components/molecules/layout/page-footer.stories.tsx
@@ -1,5 +1,4 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { MetaData } from './meta';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { PageFooter as PageFooterComponent } from './page-footer';
/**
@@ -40,16 +39,17 @@ const Template: ComponentStory<typeof PageFooterComponent> = (args) => (
<PageFooterComponent {...args} />
);
-const meta: MetaData = {
- custom: {
+const meta = [
+ {
+ id: 'more-about',
label: 'More posts about:',
- value: [
- <a key="topic-1" href="#">
+ value: (
+ <a key="topic-1" href="#topic1">
Topic name
- </a>,
- ],
+ </a>
+ ),
},
-};
+];
/**
* Page Footer Stories - With meta
diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx
index 375cbc4..a93fced 100644
--- a/src/components/molecules/layout/page-footer.tsx
+++ b/src/components/molecules/layout/page-footer.tsx
@@ -1,12 +1,12 @@
import type { FC } from 'react';
import { Footer, type FooterProps } from '../../atoms';
-import { Meta, type MetaData } from './meta';
+import { MetaList, type MetaItemData } from '../meta-list';
export type PageFooterProps = Omit<FooterProps, 'children'> & {
/**
* The footer metadata.
*/
- meta?: MetaData;
+ meta?: MetaItemData[];
};
/**
@@ -15,5 +15,7 @@ export type PageFooterProps = Omit<FooterProps, 'children'> & {
* Render a footer to display page meta.
*/
export const PageFooter: FC<PageFooterProps> = ({ meta, ...props }) => (
- <Footer {...props}>{meta ? <Meta data={meta} /> : null}</Footer>
+ <Footer {...props}>
+ {meta ? <MetaList hasInlinedValues items={meta} /> : null}
+ </Footer>
);
diff --git a/src/components/molecules/layout/page-header.stories.tsx b/src/components/molecules/layout/page-header.stories.tsx
index ea943bf..54d5fe8 100644
--- a/src/components/molecules/layout/page-header.stories.tsx
+++ b/src/components/molecules/layout/page-header.stories.tsx
@@ -1,4 +1,4 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { PageHeader } from './page-header';
/**
@@ -62,17 +62,31 @@ const Template: ComponentStory<typeof PageHeader> = (args) => (
<PageHeader {...args} />
);
-const meta = {
- publication: { date: '2022-04-09' },
- thematics: [
- <a key="category1" href="#">
- Category 1
- </a>,
- <a key="category2" href="#">
- Category 2
- </a>,
- ],
-};
+const meta = [
+ { id: 'publication-date', label: 'Published on:', value: '2022-04-09' },
+ {
+ id: 'thematics',
+ label: 'Thematics:',
+ value: [
+ {
+ id: 'cat-1',
+ value: (
+ <a key="category1" href="#cat1">
+ Category 1
+ </a>
+ ),
+ },
+ {
+ id: 'cat-2',
+ value: (
+ <a key="category2" href="#cat2">
+ Category 2
+ </a>
+ ),
+ },
+ ],
+ },
+];
/**
* Page Header Stories - Default
diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx
index b727cc1..ea0dd2c 100644
--- a/src/components/molecules/layout/page-header.tsx
+++ b/src/components/molecules/layout/page-header.tsx
@@ -1,6 +1,6 @@
import type { FC, ReactNode } from 'react';
import { Header, Heading } from '../../atoms';
-import { Meta, type MetaData } from './meta';
+import { MetaList, type MetaItemData } from '../meta-list';
import styles from './page-header.module.scss';
export type PageHeaderProps = {
@@ -15,7 +15,7 @@ export type PageHeaderProps = {
/**
* The page metadata.
*/
- meta?: MetaData;
+ meta?: MetaItemData[];
/**
* The page title.
*/
@@ -56,7 +56,7 @@ export const PageHeader: FC<PageHeaderProps> = ({
{title}
</Heading>
{meta ? (
- <Meta className={styles.meta} data={meta} isInline spacing="xs" />
+ <MetaList className={styles.meta} hasInlinedItems items={meta} />
) : null}
{intro ? getIntro() : null}
</div>
diff --git a/src/components/molecules/meta-list/index.ts b/src/components/molecules/meta-list/index.ts
new file mode 100644
index 0000000..93f437d
--- /dev/null
+++ b/src/components/molecules/meta-list/index.ts
@@ -0,0 +1,2 @@
+export * from './meta-item';
+export * from './meta-list';
diff --git a/src/components/molecules/meta-list/meta-item/index.ts b/src/components/molecules/meta-list/meta-item/index.ts
new file mode 100644
index 0000000..47795de
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-item/index.ts
@@ -0,0 +1 @@
+export * from './meta-item';
diff --git a/src/components/molecules/meta-list/meta-item/meta-item.module.scss b/src/components/molecules/meta-list/meta-item/meta-item.module.scss
new file mode 100644
index 0000000..a1c2d47
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-item/meta-item.module.scss
@@ -0,0 +1,62 @@
+@use "../../../../styles/abstracts/functions" as fun;
+
+.item {
+ column-gap: var(--spacing-2xs);
+ align-content: baseline;
+
+ &--bordered-values {
+ row-gap: var(--spacing-2xs);
+ }
+
+ &--centered {
+ margin-inline: auto;
+ text-align: center;
+ place-items: center;
+ justify-content: center;
+ }
+
+ &--inlined {
+ align-items: first baseline;
+ }
+
+ &--inlined-values {
+ flex-flow: row wrap;
+ }
+
+ &:not(#{&}--bordered-values) {
+ row-gap: fun.convert-px(3);
+ }
+}
+
+.value {
+ width: fit-content;
+ height: fit-content;
+ color: var(--color-fg);
+ font-weight: 400;
+}
+
+:where(.item--bordered-values) {
+ .value {
+ padding: fun.convert-px(2) var(--spacing-2xs);
+ border: fun.convert-px(1) solid var(--color-primary-darker);
+ }
+}
+
+:where(.item--inlined-values) {
+ .label {
+ flex: 1 0 100%;
+ }
+}
+
+/* It's an arbitrary choice. When there is only one meta item (like on small
+ * cards) removing the width can mess up the layout. However, must of the times
+ * when there are multiples items, we need to remove the width especially if we
+ * want to use `isCentered` prop. */
+:where(.item--inlined-values:not(:only-of-type)) {
+ .label {
+ /* We need to remove its width to avoid an extra space and make the
+ * container width fit its contents. However the label should be smaller
+ * than the values to avoid unexpected behavior with layout. */
+ width: 0;
+ }
+}
diff --git a/src/components/molecules/meta-list/meta-item/meta-item.stories.tsx b/src/components/molecules/meta-list/meta-item/meta-item.stories.tsx
new file mode 100644
index 0000000..3ddb8f1
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-item/meta-item.stories.tsx
@@ -0,0 +1,108 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Link } from '../../../atoms';
+import { MetaItem } from './meta-item';
+
+/**
+ * MetaItem - Storybook Meta
+ */
+export default {
+ title: 'Molecules/MetaList/Item',
+ component: MetaItem,
+ argTypes: {
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'The item label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof MetaItem>;
+
+const Template: ComponentStory<typeof MetaItem> = (args) => (
+ <MetaItem {...args} />
+);
+
+/**
+ * MetaItem Stories - SingleValue
+ */
+export const SingleValue = Template.bind({});
+SingleValue.args = {
+ label: 'Comments',
+ value: 'No comments',
+};
+
+/**
+ * MetaItem Stories - MultipleValues
+ */
+export const MultipleValues = Template.bind({});
+MultipleValues.args = {
+ label: 'Tags',
+ value: [
+ { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> },
+ { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> },
+ ],
+};
+
+/**
+ * MetaItem Stories - SingleValueBordered
+ */
+export const SingleValueBordered = Template.bind({});
+SingleValueBordered.args = {
+ hasBorderedValues: true,
+ label: 'Comments',
+ value: 'No comments',
+};
+
+/**
+ * MetaItem Stories - MultipleValuesBordered
+ */
+export const MultipleValuesBordered = Template.bind({});
+MultipleValuesBordered.args = {
+ hasBorderedValues: true,
+ label: 'Tags',
+ value: [
+ { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> },
+ { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> },
+ ],
+};
+
+/**
+ * MetaItem Stories - SingleValueInlined
+ */
+export const SingleValueInlined = Template.bind({});
+SingleValueInlined.args = {
+ isInline: true,
+ label: 'Comments',
+ value: 'No comments',
+};
+
+/**
+ * MetaItem Stories - MultipleValuesInlined
+ */
+export const MultipleValuesInlined = Template.bind({});
+MultipleValuesInlined.args = {
+ isInline: true,
+ label: 'Tags',
+ value: [
+ { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> },
+ { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> },
+ ],
+};
+
+/**
+ * MetaItem Stories - InlinedValues
+ */
+export const InlinedValues = Template.bind({});
+InlinedValues.args = {
+ hasInlinedValues: true,
+ label: 'Tags',
+ value: [
+ { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> },
+ { id: 'tag2', value: <Link href="#tag2">A long tag 2</Link> },
+ { id: 'tag3', value: <Link href="#tag3">Tag 3</Link> },
+ ],
+};
diff --git a/src/components/molecules/meta-list/meta-item/meta-item.test.tsx b/src/components/molecules/meta-list/meta-item/meta-item.test.tsx
new file mode 100644
index 0000000..629c4b2
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-item/meta-item.test.tsx
@@ -0,0 +1,97 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { MetaItem } from './meta-item';
+
+describe('MetaItem', () => {
+ it('renders a label and a value', () => {
+ const label = 'iusto';
+ const value = 'autem';
+
+ render(
+ <dl>
+ <MetaItem label={label} value={value} />
+ </dl>
+ );
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent(label);
+ expect(rtlScreen.getByRole('definition')).toHaveTextContent(value);
+ });
+
+ it('can render a label with multiple values', () => {
+ const label = 'iusto';
+ const values = [
+ { id: 'autem', value: 'autem' },
+ { id: 'quisquam', value: 'aut' },
+ { id: 'molestias', value: 'voluptatem' },
+ ];
+
+ render(
+ <dl>
+ <MetaItem label={label} value={values} />
+ </dl>
+ );
+
+ expect(rtlScreen.getByRole('term')).toHaveTextContent(label);
+ expect(rtlScreen.getAllByRole('definition')).toHaveLength(values.length);
+ });
+
+ it('can render a centered group of label and values', () => {
+ const label = 'iusto';
+ const value = 'autem';
+
+ render(
+ <dl>
+ <MetaItem isCentered label={label} value={value} />
+ </dl>
+ );
+
+ expect(rtlScreen.getByRole('term').parentElement).toHaveClass(
+ 'item--centered'
+ );
+ });
+
+ it('can render an inlined group of label and values', () => {
+ const label = 'iusto';
+ const value = 'autem';
+
+ render(
+ <dl>
+ <MetaItem isInline label={label} value={value} />
+ </dl>
+ );
+
+ expect(rtlScreen.getByRole('term').parentElement).toHaveClass(
+ 'item--inlined'
+ );
+ });
+
+ it('can render a group of label and bordered values', () => {
+ const label = 'iusto';
+ const value = 'autem';
+
+ render(
+ <dl>
+ <MetaItem hasBorderedValues label={label} value={value} />
+ </dl>
+ );
+
+ expect(rtlScreen.getByRole('term').parentElement).toHaveClass(
+ 'item--bordered-values'
+ );
+ });
+
+ it('can render a group of label and inlined values', () => {
+ const label = 'iusto';
+ const value = 'autem';
+
+ render(
+ <dl>
+ <MetaItem hasInlinedValues label={label} value={value} />
+ </dl>
+ );
+
+ expect(rtlScreen.getByRole('term').parentElement).toHaveClass(
+ 'item--inlined-values'
+ );
+ });
+});
diff --git a/src/components/molecules/meta-list/meta-item/meta-item.tsx b/src/components/molecules/meta-list/meta-item/meta-item.tsx
new file mode 100644
index 0000000..c5223c2
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-item/meta-item.tsx
@@ -0,0 +1,90 @@
+import {
+ type ForwardRefRenderFunction,
+ type ReactElement,
+ type ReactNode,
+ forwardRef,
+} from 'react';
+import { Description, Group, type GroupProps, Term } from '../../../atoms';
+import styles from './meta-item.module.scss';
+
+export type MetaValue = string | ReactElement;
+
+export type MetaValues = {
+ id: string;
+ value: MetaValue;
+};
+
+export type MetaItemProps = Omit<GroupProps, 'children' | 'spacing'> & {
+ /**
+ * Should the values be bordered?
+ *
+ * @default false
+ */
+ hasBorderedValues?: boolean;
+ /**
+ * Should the values be inlined?
+ *
+ * @warning If you use it make sure the value is larger than the label. It
+ * could mess up your design since we are removing the label width.
+ *
+ * @default false
+ */
+ hasInlinedValues?: boolean;
+ /**
+ * Should the label and values be centered?
+ *
+ * @default false
+ */
+ isCentered?: boolean;
+ /**
+ * The item label.
+ */
+ label: ReactNode;
+ /**
+ * The item value or values.
+ */
+ value: MetaValue | MetaValues[];
+};
+
+const MetaItemWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ MetaItemProps
+> = (
+ {
+ className = '',
+ hasBorderedValues = false,
+ hasInlinedValues = false,
+ isCentered = false,
+ isInline = false,
+ label,
+ value,
+ ...props
+ },
+ ref
+) => {
+ const itemClass = [
+ styles.item,
+ styles[hasBorderedValues ? 'item--bordered-values' : ''],
+ styles[hasInlinedValues ? 'item--inlined-values' : ''],
+ styles[isCentered ? 'item--centered' : ''],
+ styles[isInline ? 'item--inlined' : 'item--stacked'],
+ className,
+ ].join(' ');
+
+ return (
+ <Group {...props} className={itemClass} isInline={isInline} ref={ref}>
+ <Term className={styles.label}>{label}</Term>
+ {Array.isArray(value) ? (
+ value.map((item) => (
+ <Description className={styles.value} key={item.id}>
+ {item.value}
+ </Description>
+ ))
+ ) : (
+ <Description className={styles.value}>{value}</Description>
+ )}
+ </Group>
+ );
+};
+
+export const MetaItem = forwardRef(MetaItemWithRef);
diff --git a/src/components/molecules/meta-list/meta-list.module.scss b/src/components/molecules/meta-list/meta-list.module.scss
new file mode 100644
index 0000000..5570f4c
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-list.module.scss
@@ -0,0 +1,24 @@
+.list {
+ display: grid;
+ width: fit-content;
+ height: fit-content;
+
+ &--centered {
+ margin-inline: auto;
+ justify-items: center;
+ }
+
+ &--inlined {
+ grid-auto-flow: column;
+ grid-template-columns: repeat(
+ auto-fit,
+ min(calc(100vw - (var(--spacing-md) * 2)), 1fr)
+ );
+ column-gap: clamp(var(--spacing-lg), 3vw, var(--spacing-3xl));
+ row-gap: clamp(var(--spacing-sm), 3vw, var(--spacing-md));
+ }
+
+ &--stacked {
+ gap: var(--spacing-2xs);
+ }
+}
diff --git a/src/components/molecules/meta-list/meta-list.stories.tsx b/src/components/molecules/meta-list/meta-list.stories.tsx
new file mode 100644
index 0000000..463ec96
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-list.stories.tsx
@@ -0,0 +1,70 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Link } from '../../atoms';
+import { type MetaItemData, MetaList } from './meta-list';
+
+/**
+ * MetaList - Storybook Meta
+ */
+export default {
+ title: 'Molecules/MetaList',
+ component: MetaList,
+ argTypes: {
+ items: {
+ description: 'The meta items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof MetaList>;
+
+const Template: ComponentStory<typeof MetaList> = (args) => (
+ <MetaList {...args} />
+);
+
+const items: MetaItemData[] = [
+ { id: 'comments', label: 'Comments', value: 'No comments.' },
+ {
+ id: 'category',
+ label: 'Category',
+ value: <Link href="#cat1">Cat 1</Link>,
+ },
+ {
+ id: 'tags',
+ label: 'Tags',
+ value: [
+ { id: 'tag1', value: <Link href="#tag1">Tag 1</Link> },
+ { id: 'tag2', value: <Link href="#tag2">Tag 2</Link> },
+ ],
+ },
+ {
+ hasBorderedValues: true,
+ hasInlinedValues: true,
+ id: 'technologies',
+ label: 'Technologies',
+ value: [
+ { id: 'techno1', value: 'HTML' },
+ { id: 'techno2', value: 'CSS' },
+ { id: 'techno3', value: 'Javascript' },
+ ],
+ },
+];
+
+/**
+ * MetaList Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ items,
+};
+
+/**
+ * MetaList Stories - Inlined
+ */
+export const Inlined = Template.bind({});
+Inlined.args = {
+ isInline: true,
+ items,
+};
diff --git a/src/components/molecules/meta-list/meta-list.test.tsx b/src/components/molecules/meta-list/meta-list.test.tsx
new file mode 100644
index 0000000..cc4d2fa
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-list.test.tsx
@@ -0,0 +1,79 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { type MetaItemData, MetaList } from './meta-list';
+
+describe('MetaList', () => {
+ it('renders a list of meta items', () => {
+ const items: MetaItemData[] = [
+ { id: 'item1', label: 'Item 1', value: 'Value 1' },
+ { id: 'item2', label: 'Item 2', value: 'Value 2' },
+ { id: 'item3', label: 'Item 3', value: 'Value 3' },
+ { id: 'item4', label: 'Item 4', value: 'Value 4' },
+ ];
+
+ render(<MetaList items={items} />);
+
+ expect(rtlScreen.getAllByRole('term')).toHaveLength(items.length);
+ expect(rtlScreen.getAllByRole('definition')).toHaveLength(items.length);
+ });
+
+ it('can render a centered list of meta items', () => {
+ const items: MetaItemData[] = [
+ { id: 'item1', label: 'Item 1', value: 'Value 1' },
+ { id: 'item2', label: 'Item 2', value: 'Value 2' },
+ { id: 'item3', label: 'Item 3', value: 'Value 3' },
+ { id: 'item4', label: 'Item 4', value: 'Value 4' },
+ ];
+
+ render(<MetaList isCentered items={items} />);
+
+ const terms = rtlScreen.getAllByRole('term');
+
+ expect(terms[0].parentElement?.parentElement).toHaveClass('list--centered');
+ });
+
+ it('can render an inlined list of meta items', () => {
+ const items: MetaItemData[] = [
+ { id: 'item1', label: 'Item 1', value: 'Value 1' },
+ { id: 'item2', label: 'Item 2', value: 'Value 2' },
+ { id: 'item3', label: 'Item 3', value: 'Value 3' },
+ { id: 'item4', label: 'Item 4', value: 'Value 4' },
+ ];
+
+ render(<MetaList isInline items={items} />);
+
+ const terms = rtlScreen.getAllByRole('term');
+
+ expect(terms[0].parentElement?.parentElement).toHaveClass('list--inlined');
+ });
+
+ it('can render a list of meta items with bordered values', () => {
+ const items: MetaItemData[] = [
+ { id: 'item1', label: 'Item 1', value: 'Value 1' },
+ { id: 'item2', label: 'Item 2', value: 'Value 2' },
+ { id: 'item3', label: 'Item 3', value: 'Value 3' },
+ { id: 'item4', label: 'Item 4', value: 'Value 4' },
+ ];
+
+ render(<MetaList hasBorderedValues items={items} />);
+
+ const terms = rtlScreen.getAllByRole('term');
+
+ expect(terms[0].parentElement).toHaveClass('item--bordered-values');
+ });
+
+ it('can render a list of meta items with inlined values', () => {
+ const items: MetaItemData[] = [
+ { id: 'item1', label: 'Item 1', value: 'Value 1' },
+ { id: 'item2', label: 'Item 2', value: 'Value 2' },
+ { id: 'item3', label: 'Item 3', value: 'Value 3' },
+ { id: 'item4', label: 'Item 4', value: 'Value 4' },
+ ];
+
+ render(<MetaList hasInlinedValues items={items} />);
+
+ const terms = rtlScreen.getAllByRole('term');
+
+ expect(terms[0].parentElement).toHaveClass('item--inlined-values');
+ });
+});
diff --git a/src/components/molecules/meta-list/meta-list.tsx b/src/components/molecules/meta-list/meta-list.tsx
new file mode 100644
index 0000000..288fd9a
--- /dev/null
+++ b/src/components/molecules/meta-list/meta-list.tsx
@@ -0,0 +1,78 @@
+import { type ForwardRefRenderFunction, forwardRef } from 'react';
+import { DescriptionList, type DescriptionListProps } from '../../atoms';
+import { MetaItem, type MetaItemProps } from './meta-item';
+import styles from './meta-list.module.scss';
+
+export type MetaItemData = Pick<
+ MetaItemProps,
+ | 'hasBorderedValues'
+ | 'hasInlinedValues'
+ | 'isCentered'
+ | 'isInline'
+ | 'label'
+ | 'value'
+> & {
+ id: string;
+};
+
+export type MetaListProps = Omit<DescriptionListProps, 'children' | 'spacing'> &
+ Pick<MetaItemProps, 'hasBorderedValues' | 'hasInlinedValues'> & {
+ /**
+ * Should the items be inlined?
+ *
+ * @default false
+ */
+ hasInlinedItems?: boolean;
+ /**
+ * Should the meta be centered?
+ *
+ * @default false
+ */
+ isCentered?: boolean;
+ /**
+ * The meta items.
+ */
+ items: MetaItemData[];
+ };
+
+const MetaListWithRef: ForwardRefRenderFunction<
+ HTMLDListElement,
+ MetaListProps
+> = (
+ {
+ className = '',
+ hasBorderedValues = false,
+ hasInlinedItems = false,
+ hasInlinedValues = false,
+ isCentered = false,
+ isInline = false,
+ items,
+ ...props
+ },
+ ref
+) => {
+ const listClass = [
+ styles.list,
+ styles[isCentered ? 'list--centered' : ''],
+ styles[isInline ? 'list--inlined' : 'list--stacked'],
+ className,
+ ].join(' ');
+
+ return (
+ <DescriptionList {...props} className={listClass} ref={ref}>
+ {items.map(({ id, ...item }) => (
+ <MetaItem
+ hasBorderedValues={hasBorderedValues}
+ hasInlinedValues={hasInlinedValues}
+ isCentered={isCentered}
+ isInline={hasInlinedItems}
+ // Each item should be able to override the global settings.
+ {...item}
+ key={id}
+ />
+ ))}
+ </DescriptionList>
+ );
+};
+
+export const MetaList = forwardRef(MetaListWithRef);
diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx
index 1b5051f..03feee7 100644
--- a/src/components/organisms/layout/cards-list.stories.tsx
+++ b/src/components/organisms/layout/cards-list.stories.tsx
@@ -90,11 +90,21 @@ const items: CardsListItem[] = [
id: 'card-1',
cover: {
alt: 'card 1 picture',
- src: 'http://picsum.photos/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Velit', 'Ex', 'Alias'] },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'velit', value: 'Velit' },
+ { id: 'ex', value: 'Ex' },
+ { id: 'alias', value: 'Alias' },
+ ],
+ },
+ ],
tagline: 'Molestias ut error',
title: 'Et alias omnis',
url: '#',
@@ -103,11 +113,11 @@ const items: CardsListItem[] = [
id: 'card-2',
cover: {
alt: 'card 2 picture',
- src: 'http://picsum.photos/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Voluptas'] },
+ meta: [{ id: 'categories', label: 'Categories', value: 'Voluptas' }],
tagline: 'Quod vel accusamus',
title: 'Laboriosam doloremque mollitia',
url: '#',
@@ -116,13 +126,22 @@ const items: CardsListItem[] = [
id: 'card-3',
cover: {
alt: 'card 3 picture',
- src: 'http://picsum.photos/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: {
- thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
- },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'quisquam', value: 'Quisquam' },
+ { id: 'quia', value: 'Quia' },
+ { id: 'sapiente', value: 'Sapiente' },
+ { id: 'perspiciatis', value: '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 751a502..c9d6ae7 100644
--- a/src/components/organisms/layout/cards-list.test.tsx
+++ b/src/components/organisms/layout/cards-list.test.tsx
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
import { CardsList, type CardsListItem } from './cards-list';
const items: CardsListItem[] = [
@@ -7,11 +7,21 @@ const items: CardsListItem[] = [
id: 'card-1',
cover: {
alt: 'card 1 picture',
- src: 'http://placeimg.com/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Velit', 'Ex', 'Alias'] },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'velit', value: 'Velit' },
+ { id: 'ex', value: 'Ex' },
+ { id: 'alias', value: 'Alias' },
+ ],
+ },
+ ],
tagline: 'Molestias ut error',
title: 'Et alias omnis',
url: '#',
@@ -20,11 +30,11 @@ const items: CardsListItem[] = [
id: 'card-2',
cover: {
alt: 'card 2 picture',
- src: 'http://placeimg.com/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: { thematics: ['Voluptas'] },
+ meta: [{ id: 'categories', label: 'Categories', value: 'Voluptas' }],
tagline: 'Quod vel accusamus',
title: 'Laboriosam doloremque mollitia',
url: '#',
@@ -33,13 +43,22 @@ const items: CardsListItem[] = [
id: 'card-3',
cover: {
alt: 'card 3 picture',
- src: 'http://placeimg.com/640/480',
+ src: 'https://picsum.photos/640/480',
width: 640,
height: 480,
},
- meta: {
- thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
- },
+ meta: [
+ {
+ id: 'categories',
+ label: 'Categories',
+ value: [
+ { id: 'quisquam', value: 'Quisquam' },
+ { id: 'quia', value: 'Quia' },
+ { id: 'sapiente', value: 'Sapiente' },
+ { id: 'perspiciatis', value: 'Perspiciatis' },
+ ],
+ },
+ ],
tagline: 'Quo error eum',
title: 'Magni rem nulla',
url: '#',
@@ -49,7 +68,7 @@ const items: CardsListItem[] = [
describe('CardsList', () => {
it('renders a list of cards', () => {
render(<CardsList items={items} titleLevel={2} />);
- expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(
+ expect(rtlScreen.getAllByRole('heading', { level: 2 })).toHaveLength(
items.length
);
});
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
index ca209f5..e1ea6b5 100644
--- a/src/components/organisms/layout/comment.tsx
+++ b/src/components/organisms/layout/comment.tsx
@@ -5,9 +5,10 @@ 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 { Meta } from '../../molecules';
+import { MetaList } from '../../molecules';
import { CommentForm, type CommentFormProps } from '../forms';
import styles from './comment.module.scss';
@@ -61,6 +62,20 @@ 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({
@@ -135,16 +150,24 @@ export const UserComment: FC<UserCommentProps> = ({
<span className={styles.author}>{author.name}</span>
)}
</header>
- <Meta
+ <MetaList
className={styles.date}
- data={{
- publication: {
- date: publicationDate,
- time: publicationTime,
- target: `#comment-${id}`,
- },
- }}
isInline
+ items={[
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Comment: publication date label',
+ id: 'soj7do',
+ }),
+ value: (
+ <Link href={`#comment-${id}`}>
+ <time dateTime={isoDateTime}>{commentDate}</time>
+ </Link>
+ ),
+ },
+ ]}
/>
<div
className={styles.body}
diff --git a/src/components/organisms/layout/overview.module.scss b/src/components/organisms/layout/overview.module.scss
index 59ce167..c1d9463 100644
--- a/src/components/organisms/layout/overview.module.scss
+++ b/src/components/organisms/layout/overview.module.scss
@@ -11,7 +11,7 @@
auto-fit,
min(calc(100vw - (var(--spacing-md) * 2)), 23ch)
);
- row-gap: var(--spacing-2xs);
+ row-gap: var(--spacing-sm);
@include mix.media("screen") {
@include mix.dimensions("md") {
@@ -21,21 +21,6 @@
);
}
}
-
- &--has-techno {
- div:last-child {
- gap: var(--spacing-2xs);
-
- dd {
- padding: 0 var(--spacing-2xs);
- border: fun.convert-px(1) solid var(--color-border-dark);
-
- &::before {
- display: none;
- }
- }
- }
- }
}
.cover {
diff --git a/src/components/organisms/layout/overview.stories.tsx b/src/components/organisms/layout/overview.stories.tsx
index 8f56d3a..562d7c4 100644
--- a/src/components/organisms/layout/overview.stories.tsx
+++ b/src/components/organisms/layout/overview.stories.tsx
@@ -1,5 +1,6 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import { Overview, type OverviewMeta } from './overview';
+import type { MetaItemData } from '../../molecules';
+import { Overview } from './overview';
/**
* Overview - Storybook Meta
@@ -54,10 +55,10 @@ const cover = {
width: 640,
};
-const meta: OverviewMeta = {
- creation: { date: '2022-05-09' },
- license: 'Dignissimos ratione veritatis',
-};
+const meta = [
+ { id: 'creation-date', label: 'Creation date', value: '2022-05-09' },
+ { id: 'license', label: 'License', value: 'Dignissimos ratione veritatis' },
+] satisfies MetaItemData[];
/**
* Overview Stories - Default
diff --git a/src/components/organisms/layout/overview.test.tsx b/src/components/organisms/layout/overview.test.tsx
index 0f2af7b..b98bd6f 100644
--- a/src/components/organisms/layout/overview.test.tsx
+++ b/src/components/organisms/layout/overview.test.tsx
@@ -1,27 +1,33 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { Overview, type OverviewMeta } from './overview';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import type { MetaItemData } from '../../molecules';
+import { Overview } from './overview';
const cover = {
alt: 'Incidunt unde quam',
height: 480,
- src: 'http://placeimg.com/640/480/cats',
+ src: 'https://picsum.photos/640/480',
width: 640,
};
-const data: OverviewMeta = {
- creation: { date: '2022-05-09' },
- license: 'Dignissimos ratione veritatis',
-};
+const meta = [
+ { id: 'creation-date', label: 'Creation date', value: '2022-05-09' },
+ { id: 'license', label: 'License', value: 'Dignissimos ratione veritatis' },
+] satisfies MetaItemData[];
describe('Overview', () => {
- it('renders some data', () => {
- render(<Overview meta={data} />);
- expect(screen.getByText(data.license!)).toBeInTheDocument();
+ it('renders some meta', () => {
+ render(<Overview meta={meta} />);
+
+ const metaLabels = meta.map((item) => item.label);
+
+ for (const label of metaLabels) {
+ expect(rtlScreen.getByText(label)).toBeInTheDocument();
+ }
});
it('renders a cover', () => {
- render(<Overview cover={cover} meta={data} />);
- expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+ render(<Overview cover={cover} meta={meta} />);
+ expect(rtlScreen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
});
});
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index 8af58ec..ede2627 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -1,19 +1,9 @@
import NextImage, { type ImageProps as NextImageProps } from 'next/image';
import type { FC } from 'react';
import { Figure } from '../../atoms';
-import { Meta, type MetaData } from '../../molecules';
+import { MetaList, type MetaItemData } from '../../molecules';
import styles from './overview.module.scss';
-export type OverviewMeta = Pick<
- MetaData,
- | 'creation'
- | 'license'
- | 'popularity'
- | 'repositories'
- | 'technologies'
- | 'update'
->;
-
export type OverviewProps = {
/**
* Set additional classnames to the overview wrapper.
@@ -26,7 +16,7 @@ export type OverviewProps = {
/**
* The overview meta.
*/
- meta: OverviewMeta;
+ meta: MetaItemData[];
};
/**
@@ -39,20 +29,16 @@ export const Overview: FC<OverviewProps> = ({
cover,
meta,
}) => {
- const { technologies, ...remainingMeta } = meta;
- const metaModifier = technologies ? styles['meta--has-techno'] : '';
+ const wrapperClass = `${styles.wrapper} ${className}`;
return (
- <div className={`${styles.wrapper} ${className}`}>
+ <div className={wrapperClass}>
{cover ? (
<Figure>
<NextImage {...cover} className={styles.cover} />
</Figure>
) : null}
- <Meta
- className={`${styles.meta} ${metaModifier}`}
- data={{ ...remainingMeta, technologies }}
- />
+ <MetaList className={styles.meta} hasInlinedValues items={meta} />
</div>
);
};
diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss
index 9dc1a69..ffc30ac 100644
--- a/src/components/organisms/layout/summary.module.scss
+++ b/src/components/organisms/layout/summary.module.scss
@@ -109,13 +109,9 @@
flex-flow: row wrap;
font-size: var(--font-size-sm);
- &__item {
- flex: 1 0 min(calc(100vw - 2 * var(--spacing-md)), 14ch);
- }
-
@include mix.media("screen") {
@include mix.dimensions("sm") {
- display: flex;
+ flex-flow: column wrap;
margin-top: 0;
}
}
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index fa3dfe5..f5c16cd 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -2,6 +2,7 @@ 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,7 +12,7 @@ import {
Link,
Figure,
} from '../../atoms';
-import { Meta, type MetaData } from '../../molecules';
+import { MetaList, type MetaItemData } from '../../molecules';
import styles from './summary.module.scss';
export type Cover = Pick<NextImageProps, 'alt' | 'src' | 'width' | 'height'>;
@@ -69,42 +70,134 @@ export const Summary: FC<SummaryProps> = ({
),
}
);
- const { author, commentsCount, cover, dates, thematics, topics, wordsCount } =
- meta;
- const readingTime = useReadingTime(wordsCount, true);
+ const readingTime = useReadingTime(meta.wordsCount, true);
- const getMeta = (): MetaData => {
- return {
- author: author?.name,
- publication: { date: dates.publication },
- update:
- dates.update && dates.publication !== dates.update
- ? { date: dates.update }
- : undefined,
- readingTime,
- thematics: thematics?.map((thematic) => (
- <Link key={thematic.id} href={thematic.url}>
- {thematic.name}
- </Link>
- )),
- topics: topics?.map((topic) => (
- <Link key={topic.id} href={topic.url}>
- {topic.name}
- </Link>
- )),
- comments: {
- about: title,
- count: commentsCount ?? 0,
- target: `${url}#comments`,
+ /**
+ * 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[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Summary: publication date label',
+ id: 'TvQ2Ee',
+ }),
+ value: getDate(meta.dates.publication),
},
- };
+ ];
+
+ if (meta.dates.update && meta.dates.update !== meta.dates.publication)
+ summaryMeta.push({
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'Summary: update date label',
+ id: 'f0Z/Po',
+ }),
+ value: getDate(meta.dates.update),
+ });
+
+ summaryMeta.push({
+ id: 'reading-time',
+ label: intl.formatMessage({
+ defaultMessage: 'Reading time:',
+ description: 'Summary: reading time label',
+ id: 'tyzdql',
+ }),
+ value: readingTime,
+ });
+
+ if (meta.author)
+ summaryMeta.push({
+ id: 'author',
+ label: intl.formatMessage({
+ defaultMessage: 'Written by:',
+ description: 'Summary: author label',
+ id: 'r/6HOI',
+ }),
+ value: meta.author.name,
+ });
+
+ if (meta.thematics)
+ summaryMeta.push({
+ id: 'thematics',
+ label: intl.formatMessage({
+ defaultMessage: 'Thematics:',
+ description: 'Summary: thematics label',
+ id: 'bk0WOp',
+ }),
+ value: meta.thematics.map((thematic) => {
+ return {
+ id: `thematic-${thematic.id}`,
+ value: <Link href={thematic.url}>{thematic.name}</Link>,
+ };
+ }),
+ });
+
+ if (meta.topics)
+ summaryMeta.push({
+ id: 'topics',
+ label: intl.formatMessage({
+ defaultMessage: 'Topics:',
+ description: 'Summary: topics label',
+ id: 'yIZ+AC',
+ }),
+ value: meta.topics.map((topic) => {
+ return {
+ id: `topic-${topic.id}`,
+ value: <Link href={topic.url}>{topic.name}</Link>,
+ };
+ }),
+ });
+
+ if (meta.commentsCount !== undefined) {
+ const commentsCount = intl.formatMessage(
+ {
+ defaultMessage:
+ '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>',
+ description: 'Summary: comments count',
+ id: 'ye/vlA',
+ },
+ {
+ a11y: (chunks: ReactNode) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ commentsCount: meta.commentsCount,
+ title,
+ }
+ );
+ summaryMeta.push({
+ id: 'comments-count',
+ label: intl.formatMessage({
+ defaultMessage: 'Comments:',
+ description: 'Summary: comments label',
+ id: 'bfPp0g',
+ }),
+ value: (
+ <Link href={`${url}#comments`}>{commentsCount as JSX.Element}</Link>
+ ),
+ });
+ }
+
+ return summaryMeta;
};
return (
<article className={styles.wrapper}>
- {cover ? (
+ {meta.cover ? (
<Figure>
- <NextImage {...cover} className={styles.cover} />
+ <NextImage {...meta.cover} className={styles.cover} />
</Figure>
) : null}
<header className={styles.header}>
@@ -121,21 +214,19 @@ export const Summary: FC<SummaryProps> = ({
dangerouslySetInnerHTML={{ __html: intro }}
/>
<ButtonLink className={styles['read-more']} to={url}>
- <>
- {readMore}
- <Icon
- aria-hidden={true}
- className={styles.icon}
- // eslint-disable-next-line react/jsx-no-literals -- Direction allowed
- orientation="right"
- // eslint-disable-next-line react/jsx-no-literals -- Shape allowed
- shape="arrow"
- />
- </>
+ {readMore}
+ <Icon
+ aria-hidden={true}
+ className={styles.icon}
+ // eslint-disable-next-line react/jsx-no-literals -- Direction allowed
+ orientation="right"
+ // eslint-disable-next-line react/jsx-no-literals -- Shape allowed
+ shape="arrow"
+ />
</ButtonLink>
</div>
<footer className={styles.footer}>
- <Meta className={styles.meta} data={getMeta()} spacing="xs" />
+ <MetaList className={styles.meta} items={getMetaItems()} />
</footer>
</article>
);
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index 683b6b2..7977382 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -271,23 +271,38 @@ Post.args = {
breadcrumb: postBreadcrumb,
title: pageTitle,
intro: pageIntro,
- headerMeta: {
- publication: { date: '2020-03-14' },
- thematics: [
- <Link key="cat1" href="#">
- Cat 1
- </Link>,
- <Link key="cat2" href="#">
- Cat 2
- </Link>,
- ],
- },
- footerMeta: {
- custom: {
+ headerMeta: [
+ { id: 'publication-date', label: 'Published on:', value: '2020-03-14' },
+ {
+ id: 'thematics',
+ label: 'Thematics:',
+ value: [
+ {
+ id: 'cat-1',
+ value: (
+ <Link key="cat1" href="#">
+ Cat 1
+ </Link>
+ ),
+ },
+ {
+ id: 'cat-2',
+ value: (
+ <Link key="cat2" href="#">
+ Cat 2
+ </Link>
+ ),
+ },
+ ],
+ },
+ ],
+ footerMeta: [
+ {
+ id: 'read-more',
label: 'Read more about:',
value: <ButtonLink to="#">Topic 1</ButtonLink>,
},
- },
+ ],
children: (
<>
<Heading level={2}>Impedit commodi rerum</Heading>
@@ -357,7 +372,7 @@ export const Blog = Template.bind({});
Blog.args = {
breadcrumb: postsListBreadcrumb,
title: 'Blog',
- headerMeta: { total: posts.length },
+ headerMeta: [{ id: 'total', label: 'Total:', value: `${posts.length}` }],
children: (
<PostsList
posts={posts}
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index ee3fd3a..dbac43e 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -16,7 +16,6 @@ import { Heading, Notice, type NoticeKind, Sidebar } from '../../atoms';
import {
Breadcrumb,
type BreadcrumbItem,
- type MetaData,
PageFooter,
type PageFooterProps,
PageHeader,
@@ -41,13 +40,6 @@ const hasComments = (
): comments is SingleComment[] =>
Array.isArray(comments) && comments.length > 0;
-/**
- * Check if meta properties are defined.
- *
- * @param {MetaData} meta - The metadata.
- */
-const hasMeta = (meta: MetaData) => Object.values(meta).every((value) => value);
-
type CommentStatus = {
isReply: boolean;
kind: NoticeKind;
@@ -256,7 +248,7 @@ export const PageLayout: FC<PageLayoutProps> = ({
{children}
</div>
)}
- {footerMeta && hasMeta(footerMeta) ? (
+ {footerMeta?.length ? (
<PageFooter meta={footerMeta} className={styles.footer} />
) : null}
<Sidebar
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 9c33d2a..92a0c45 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -31,10 +31,6 @@
"defaultMessage": "Related thematics",
"description": "TopicPage: related thematics list widget title"
},
- "02rgLO": {
- "defaultMessage": "{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>",
- "description": "Meta: comments count"
- },
"0gVlI3": {
"defaultMessage": "Tracking:",
"description": "AckeeToggle: select label"
@@ -59,6 +55,10 @@
"defaultMessage": "Name:",
"description": "ContactForm: name label"
},
+ "24FIsG": {
+ "defaultMessage": "Updated on:",
+ "description": "ThematicPage: update date label"
+ },
"28GZdv": {
"defaultMessage": "Projects",
"description": "Breadcrumb: projects label"
@@ -99,6 +99,10 @@
"defaultMessage": "Page not found.",
"description": "404Page: SEO - Meta description"
},
+ "4QbTDq": {
+ "defaultMessage": "Published on:",
+ "description": "Page: publication date label"
+ },
"4iYISO": {
"defaultMessage": "Loading the requested article...",
"description": "ArticlePage: loading article message"
@@ -151,9 +155,9 @@
"defaultMessage": "{website} picture",
"description": "Layout: photo alternative text"
},
- "92zgdp": {
- "defaultMessage": "Total:",
- "description": "Meta: total label"
+ "9DfuHk": {
+ "defaultMessage": "Updated on:",
+ "description": "TopicPage: update date label"
},
"9MeLN3": {
"defaultMessage": "{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}",
@@ -179,10 +183,6 @@
"defaultMessage": "Contact",
"description": "ContactPage: page title"
},
- "AuGklx": {
- "defaultMessage": "License:",
- "description": "Meta: license label"
- },
"B290Ph": {
"defaultMessage": "Thanks, your comment was successfully sent.",
"description": "PageLayout: comment form success message"
@@ -199,6 +199,10 @@
"defaultMessage": "Failed to load.",
"description": "BlogPage: failed to load text"
},
+ "CvOqoh": {
+ "defaultMessage": "Thematics:",
+ "description": "ArticlePage: thematics meta label"
+ },
"D8vB38": {
"defaultMessage": "Blog",
"description": "Layout: main nav - blog link"
@@ -211,14 +215,6 @@
"defaultMessage": "Thematics",
"description": "SearchPage: thematics list widget title"
},
- "DssFG1": {
- "defaultMessage": "Repositories:",
- "description": "Meta: repositories label"
- },
- "EbFvsM": {
- "defaultMessage": "Reading time:",
- "description": "Meta: reading time label"
- },
"EeCqAE": {
"defaultMessage": "Loading the search results...",
"description": "SearchPage: loading search results message"
@@ -227,14 +223,14 @@
"defaultMessage": "Blog",
"description": "Breadcrumb: blog label"
},
+ "Ez8Qim": {
+ "defaultMessage": "Updated on:",
+ "description": "Page: update date label"
+ },
"G+Twgm": {
"defaultMessage": "Search",
"description": "SearchModal: modal title"
},
- "GRyyfy": {
- "defaultMessage": "Official website:",
- "description": "Meta: official website label"
- },
"GTbGMy": {
"defaultMessage": "Open menu",
"description": "MainNav: Open label"
@@ -243,6 +239,10 @@
"defaultMessage": "Topics",
"description": "Error404Page: topics list widget title"
},
+ "Gw7X3x": {
+ "defaultMessage": "Reading time:",
+ "description": "ArticlePage: reading time label"
+ },
"HFdzae": {
"defaultMessage": "Contact form",
"description": "ContactForm: form accessible name"
@@ -255,6 +255,10 @@
"defaultMessage": "Thematics",
"description": "BlogPage: thematics list widget title"
},
+ "HxZvY4": {
+ "defaultMessage": "Published on:",
+ "description": "ProjectsPage: publication date label"
+ },
"IY5ew6": {
"defaultMessage": "Submitting...",
"description": "CommentForm: spinner message on submit"
@@ -271,6 +275,10 @@
"defaultMessage": "Skip to content",
"description": "Layout: Skip to content link"
},
+ "KV+NMZ": {
+ "defaultMessage": "Published on:",
+ "description": "TopicPage: publication date label"
+ },
"KVSWGP": {
"defaultMessage": "Other thematics",
"description": "ThematicPage: other thematics list widget title"
@@ -279,6 +287,10 @@
"defaultMessage": "Page not found",
"description": "Error404Page: page title"
},
+ "KrNvQi": {
+ "defaultMessage": "Popularity:",
+ "description": "ProjectsPage: popularity label"
+ },
"LCorTC": {
"defaultMessage": "Cancel reply",
"description": "Comment: cancel reply button"
@@ -287,10 +299,18 @@
"defaultMessage": "Close search",
"description": "Search: Close label"
},
+ "Ld6yMP": {
+ "defaultMessage": "{date} at {time}",
+ "description": "Comment: publication date and time"
+ },
"LszkU6": {
"defaultMessage": "All posts in {thematicName}",
"description": "ThematicPage: posts list heading"
},
+ "MJbZfX": {
+ "defaultMessage": "Written by:",
+ "description": "ArticlePage: author label"
+ },
"N44SOc": {
"defaultMessage": "Projects",
"description": "HomePage: link to projects"
@@ -307,18 +327,10 @@
"defaultMessage": "Github profile",
"description": "ProjectsPage: Github profile link"
},
- "OF5cPz": {
- "defaultMessage": "{postsCount, plural, =0 {No articles} one {# article} other {# articles}}",
- "description": "BlogPage: posts count meta"
- },
"OHvb01": {
"defaultMessage": "Back to top",
"description": "SiteFooter: an accessible name for the back to top button"
},
- "OI0N37": {
- "defaultMessage": "Written by:",
- "description": "Meta: author label"
- },
"OL0Yzx": {
"defaultMessage": "Publish",
"description": "CommentForm: submit button"
@@ -343,10 +355,6 @@
"defaultMessage": "Open settings",
"description": "Settings: Open label"
},
- "QGi5uD": {
- "defaultMessage": "Published on:",
- "description": "Meta: publication date label"
- },
"QLisK6": {
"defaultMessage": "Dark Theme 🌙",
"description": "usePrism: toggle dark theme button text"
@@ -363,10 +371,22 @@
"defaultMessage": "CV",
"description": "Layout: main nav - cv link"
},
+ "RecdwX": {
+ "defaultMessage": "Published on:",
+ "description": "ArticlePage: publication date label"
+ },
+ "RvGb2c": {
+ "defaultMessage": "{postsCount, plural, =0 {No articles} one {# article} other {# articles}}",
+ "description": "Page: posts count meta"
+ },
"RwI3B9": {
"defaultMessage": "Loading the repository popularity...",
"description": "ProjectsPage: loading repository popularity"
},
+ "RwNZ6p": {
+ "defaultMessage": "Technologies:",
+ "description": "ProjectsPage: technologies label"
+ },
"Sm2wCk": {
"defaultMessage": "LinkedIn profile",
"description": "CVPage: LinkedIn profile link"
@@ -383,6 +403,14 @@
"defaultMessage": "An error occurred:",
"description": "Contact: error message"
},
+ "TvQ2Ee": {
+ "defaultMessage": "Published on:",
+ "description": "Summary: publication date label"
+ },
+ "UTGhUU": {
+ "defaultMessage": "Published on:",
+ "description": "ThematicPage: publication date label"
+ },
"UsQske": {
"defaultMessage": "Read more here:",
"description": "Sharing: content link prefix"
@@ -395,6 +423,10 @@
"defaultMessage": "It is now awaiting moderation.",
"description": "PageLayout: comment awaiting moderation"
},
+ "VtYzuv": {
+ "defaultMessage": "License:",
+ "description": "ProjectsPage: license label"
+ },
"WDwNDl": {
"defaultMessage": "Search",
"description": "SearchPage: SEO - Page title"
@@ -427,6 +459,10 @@
"defaultMessage": "Light theme",
"description": "ThemeToggle: light theme label"
},
+ "ZAqGZ6": {
+ "defaultMessage": "Updated on:",
+ "description": "ArticlePage: update date label"
+ },
"ZB/Aw2": {
"defaultMessage": "Partial includes only page url, views and duration.",
"description": "AckeeToggle: tooltip message"
@@ -455,18 +491,18 @@
"defaultMessage": "You should read {title}",
"description": "Sharing: subject text"
},
- "b4fdYE": {
- "defaultMessage": "Created on:",
- "description": "Meta: creation date label"
+ "bfPp0g": {
+ "defaultMessage": "Comments:",
+ "description": "Summary: comments label"
+ },
+ "bk0WOp": {
+ "defaultMessage": "Thematics:",
+ "description": "Summary: thematics label"
},
"bojYF5": {
"defaultMessage": "Home",
"description": "Layout: main nav - home link"
},
- "bz53Us": {
- "defaultMessage": "Thematics:",
- "description": "Meta: thematics label"
- },
"c556Qo": {
"defaultMessage": "Sidebar",
"description": "PageLayout: accessible name for the sidebar"
@@ -475,6 +511,10 @@
"defaultMessage": "Comment form",
"description": "CommentForm: aria label"
},
+ "f0Z/Po": {
+ "defaultMessage": "Updated on:",
+ "description": "Summary: update date label"
+ },
"fN04AJ": {
"defaultMessage": "<link>Download the CV in PDF</link>",
"description": "CVPage: download CV in PDF text"
@@ -483,10 +523,6 @@
"defaultMessage": "Failed to load.",
"description": "SearchPage: failed to load text"
},
- "fcHeyC": {
- "defaultMessage": "{date} at {time}",
- "description": "Meta: publication date and time"
- },
"fkcTGp": {
"defaultMessage": "An error occurred:",
"description": "PageLayout: comment form error message"
@@ -499,10 +535,6 @@
"defaultMessage": "It has been approved.",
"description": "PageLayout: comment approved."
},
- "gJNaBD": {
- "defaultMessage": "Topics:",
- "description": "Meta: topics label"
- },
"gPfT/K": {
"defaultMessage": "Settings",
"description": "SettingsModal: title"
@@ -523,6 +555,14 @@
"defaultMessage": "{count} seconds",
"description": "useReadingTime: seconds count"
},
+ "iDIKb7": {
+ "defaultMessage": "Repositories:",
+ "description": "ProjectsPage: repositories label"
+ },
+ "iv3Ex1": {
+ "defaultMessage": "{postsCount, plural, =0 {No articles} one {# article} other {# articles}}",
+ "description": "ThematicPage: posts count meta"
+ },
"j5k9Fe": {
"defaultMessage": "Home",
"description": "Breadcrumb: home label"
@@ -531,14 +571,18 @@
"defaultMessage": "Linux",
"description": "HomePage: link to Linux thematic"
},
- "jTVIh8": {
- "defaultMessage": "Comments:",
- "description": "Meta: comments label"
+ "kNBXyK": {
+ "defaultMessage": "Total:",
+ "description": "Page: total label"
},
"kzIYoQ": {
"defaultMessage": "Leave a comment",
"description": "PageLayout: comment form title"
},
+ "lHkta9": {
+ "defaultMessage": "Total:",
+ "description": "ThematicPage: total label"
+ },
"lKhTGM": {
"defaultMessage": "Use Ctrl+c to copy",
"description": "usePrism: copy button error text"
@@ -575,14 +619,14 @@
"defaultMessage": "Footer",
"description": "SiteFooter: an accessible name for the footer nav"
},
+ "pT5nHk": {
+ "defaultMessage": "Published on:",
+ "description": "HomePage: publication date label"
+ },
"pWKyyR": {
"defaultMessage": "Off",
"description": "MotionToggle: deactivate reduce motion label"
},
- "pWTj2W": {
- "defaultMessage": "Popularity:",
- "description": "Meta: popularity label"
- },
"pg26sn": {
"defaultMessage": "Discover search results for {query} on {websiteName}.",
"description": "SearchPage: SEO - Meta description"
@@ -595,6 +639,10 @@
"defaultMessage": "Projects",
"description": "Layout: main nav - projects link"
},
+ "r/6HOI": {
+ "defaultMessage": "Written by:",
+ "description": "Summary: author label"
+ },
"s1i43J": {
"defaultMessage": "{minutesCount} minutes",
"description": "useReadingTime: rounded minutes count"
@@ -619,18 +667,22 @@
"defaultMessage": "Contact me",
"description": "HomePage: contact button text"
},
+ "soj7do": {
+ "defaultMessage": "Published on:",
+ "description": "Comment: publication date label"
+ },
"suXOBu": {
"defaultMessage": "Theme:",
"description": "ThemeToggle: theme label"
},
+ "tBX4mb": {
+ "defaultMessage": "Total:",
+ "description": "TopicPage: total label"
+ },
"tIZYpD": {
"defaultMessage": "Partial",
"description": "AckeeToggle: partial option name"
},
- "tLC7bh": {
- "defaultMessage": "Updated on:",
- "description": "Meta: update date label"
- },
"tMuNTy": {
"defaultMessage": "{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.",
"description": "HomePage: SEO - Meta description"
@@ -639,10 +691,18 @@
"defaultMessage": "Light theme",
"description": "PrismThemeToggle: light theme label"
},
+ "tyzdql": {
+ "defaultMessage": "Reading time:",
+ "description": "Summary: reading time label"
+ },
"u41qSk": {
"defaultMessage": "Website:",
"description": "CommentForm: website label"
},
+ "uAL4iW": {
+ "defaultMessage": "{postsCount, plural, =0 {No articles} one {# article} other {# articles}}",
+ "description": "TopicPage: posts count meta"
+ },
"uaqd5F": {
"defaultMessage": "Load more articles?",
"description": "PostsList: load more button"
@@ -667,6 +727,14 @@
"defaultMessage": "Free",
"description": "HomePage: link to free thematic"
},
+ "wQrvgw": {
+ "defaultMessage": "Updated on:",
+ "description": "ProjectsPage: update date label"
+ },
+ "wVFA4m": {
+ "defaultMessage": "Created on:",
+ "description": "ProjectsPage: creation date label"
+ },
"xYNeKX": {
"defaultMessage": "Settings form",
"description": "SettingsModal: an accessible form name"
@@ -687,10 +755,18 @@
"defaultMessage": "You are here:",
"description": "Pagination: current page indication"
},
+ "yIZ+AC": {
+ "defaultMessage": "Topics:",
+ "description": "Summary: topics label"
+ },
"yN5P+m": {
"defaultMessage": "Message:",
"description": "ContactForm: message label"
},
+ "ye/vlA": {
+ "defaultMessage": "{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>",
+ "description": "Summary: comments count"
+ },
"yfgMcl": {
"defaultMessage": "Introduction:",
"description": "Sharing: email content prefix"
@@ -702,5 +778,9 @@
"zbzlb1": {
"defaultMessage": "Page {number}",
"description": "BlogPage: page number"
+ },
+ "zoifQd": {
+ "defaultMessage": "Official website:",
+ "description": "TopicPage: official website label"
}
}
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 997e0e0..f602b20 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -31,10 +31,6 @@
"defaultMessage": "Thématiques liées",
"description": "TopicPage: related thematics list widget title"
},
- "02rgLO": {
- "defaultMessage": "{commentsCount, plural, =0 {0 commentaire} one {# commentaire} other {# commentaires}}<a11y> à propos de {title}</a11y>",
- "description": "Meta: comments count"
- },
"0gVlI3": {
"defaultMessage": "Suivi :",
"description": "AckeeToggle: select label"
@@ -59,6 +55,10 @@
"defaultMessage": "Nom :",
"description": "ContactForm: name label"
},
+ "24FIsG": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "ThematicPage: update date label"
+ },
"28GZdv": {
"defaultMessage": "Projets",
"description": "Breadcrumb: projects label"
@@ -99,6 +99,10 @@
"defaultMessage": "Page non trouvée.",
"description": "404Page: SEO - Meta description"
},
+ "4QbTDq": {
+ "defaultMessage": "Publié le :",
+ "description": "Page: publication date label"
+ },
"4iYISO": {
"defaultMessage": "Chargement de l’article demandé…",
"description": "ArticlePage: loading article message"
@@ -151,9 +155,9 @@
"defaultMessage": "Photo d’{website}",
"description": "Layout: photo alternative text"
},
- "92zgdp": {
- "defaultMessage": "Total :",
- "description": "Meta: total label"
+ "9DfuHk": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "TopicPage: update date label"
},
"9MeLN3": {
"defaultMessage": "{articlesCount, plural, =0 {# article chargé} one {# article chargé} other {# articles chargés}} sur un total de {total}",
@@ -179,10 +183,6 @@
"defaultMessage": "Contact",
"description": "ContactPage: page title"
},
- "AuGklx": {
- "defaultMessage": "Licence :",
- "description": "Meta: license label"
- },
"B290Ph": {
"defaultMessage": "Merci, votre commentaire a été envoyé avec succès.",
"description": "PageLayout: comment form success message"
@@ -199,6 +199,10 @@
"defaultMessage": "Échec du chargement.",
"description": "BlogPage: failed to load text"
},
+ "CvOqoh": {
+ "defaultMessage": "Thématiques :",
+ "description": "ArticlePage: thematics meta label"
+ },
"D8vB38": {
"defaultMessage": "Blog",
"description": "Layout: main nav - blog link"
@@ -211,14 +215,6 @@
"defaultMessage": "Thématiques",
"description": "SearchPage: thematics list widget title"
},
- "DssFG1": {
- "defaultMessage": "Dépôts :",
- "description": "Meta: repositories label"
- },
- "EbFvsM": {
- "defaultMessage": "Temps de lecture :",
- "description": "Meta: reading time label"
- },
"EeCqAE": {
"defaultMessage": "Chargement des résultats…",
"description": "SearchPage: loading search results message"
@@ -227,14 +223,14 @@
"defaultMessage": "Blog",
"description": "Breadcrumb: blog label"
},
+ "Ez8Qim": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "Page: update date label"
+ },
"G+Twgm": {
"defaultMessage": "Recherche",
"description": "SearchModal: modal title"
},
- "GRyyfy": {
- "defaultMessage": "Site officiel :",
- "description": "Meta: official website label"
- },
"GTbGMy": {
"defaultMessage": "Ouvrir le menu",
"description": "MainNav: Open label"
@@ -243,6 +239,10 @@
"defaultMessage": "Sujets",
"description": "Error404Page: topics list widget title"
},
+ "Gw7X3x": {
+ "defaultMessage": "Temps de lecture :",
+ "description": "ArticlePage: reading time label"
+ },
"HFdzae": {
"defaultMessage": "Formulaire de contact",
"description": "ContactForm: form accessible name"
@@ -255,6 +255,10 @@
"defaultMessage": "Thématiques",
"description": "BlogPage: thematics list widget title"
},
+ "HxZvY4": {
+ "defaultMessage": "Publié le :",
+ "description": "ProjectsPage: publication date label"
+ },
"IY5ew6": {
"defaultMessage": "En cours d’envoi…",
"description": "CommentForm: spinner message on submit"
@@ -271,6 +275,10 @@
"defaultMessage": "Aller au contenu",
"description": "Layout: Skip to content link"
},
+ "KV+NMZ": {
+ "defaultMessage": "Publié le :",
+ "description": "TopicPage: publication date label"
+ },
"KVSWGP": {
"defaultMessage": "Autres thématiques",
"description": "ThematicPage: other thematics list widget title"
@@ -279,6 +287,10 @@
"defaultMessage": "Page non trouvée",
"description": "Error404Page: page title"
},
+ "KrNvQi": {
+ "defaultMessage": "Popularité :",
+ "description": "ProjectsPage: popularity label"
+ },
"LCorTC": {
"defaultMessage": "Annuler la réponse",
"description": "Comment: cancel reply button"
@@ -287,10 +299,18 @@
"defaultMessage": "Fermer la recherche",
"description": "Search: Close label"
},
+ "Ld6yMP": {
+ "defaultMessage": "{date} à {time}",
+ "description": "Comment: publication date and time"
+ },
"LszkU6": {
"defaultMessage": "Tous les articles dans {thematicName}",
"description": "ThematicPage: posts list heading"
},
+ "MJbZfX": {
+ "defaultMessage": "Écrit par :",
+ "description": "ArticlePage: author label"
+ },
"N44SOc": {
"defaultMessage": "Projets",
"description": "HomePage: link to projects"
@@ -307,18 +327,10 @@
"defaultMessage": "Profil Github",
"description": "ProjectsPage: Github profile link"
},
- "OF5cPz": {
- "defaultMessage": "{postsCount, plural, =0 {0 article} one {# article} other {# articles}}",
- "description": "BlogPage: posts count meta"
- },
"OHvb01": {
"defaultMessage": "Retour en haut de page",
"description": "SiteFooter: an accessible name for the back to top button"
},
- "OI0N37": {
- "defaultMessage": "Écrit par :",
- "description": "Meta: author label"
- },
"OL0Yzx": {
"defaultMessage": "Publier",
"description": "CommentForm: submit button"
@@ -343,10 +355,6 @@
"defaultMessage": "Ouvrir les réglages",
"description": "Settings: Open label"
},
- "QGi5uD": {
- "defaultMessage": "Publié le :",
- "description": "Meta: publication date label"
- },
"QLisK6": {
"defaultMessage": "Thème sombre 🌙",
"description": "usePrism: toggle dark theme button text"
@@ -363,10 +371,22 @@
"defaultMessage": "CV",
"description": "Layout: main nav - cv link"
},
+ "RecdwX": {
+ "defaultMessage": "Publié le :",
+ "description": "ArticlePage: publication date label"
+ },
+ "RvGb2c": {
+ "defaultMessage": "{postsCount, plural, =0 {0 article} one {# article} other {# articles}}",
+ "description": "Page: posts count meta"
+ },
"RwI3B9": {
"defaultMessage": "Chargement de la popularité du dépôt…",
"description": "ProjectsPage: loading repository popularity"
},
+ "RwNZ6p": {
+ "defaultMessage": "Technologies :",
+ "description": "ProjectsPage: technologies label"
+ },
"Sm2wCk": {
"defaultMessage": "Profil LinkedIn",
"description": "CVPage: LinkedIn profile link"
@@ -383,6 +403,14 @@
"defaultMessage": "Une erreur est survenue :",
"description": "Contact: error message"
},
+ "TvQ2Ee": {
+ "defaultMessage": "Publié le :",
+ "description": "Summary: publication date label"
+ },
+ "UTGhUU": {
+ "defaultMessage": "Publié le :",
+ "description": "ThematicPage: publication date label"
+ },
"UsQske": {
"defaultMessage": "En lire plus ici :",
"description": "Sharing: content link prefix"
@@ -395,6 +423,10 @@
"defaultMessage": "Il est maintenant en attente de modération.",
"description": "PageLayout: comment awaiting moderation"
},
+ "VtYzuv": {
+ "defaultMessage": "License :",
+ "description": "ProjectsPage: license label"
+ },
"WDwNDl": {
"defaultMessage": "Recherche",
"description": "SearchPage: SEO - Page title"
@@ -427,6 +459,10 @@
"defaultMessage": "Thème clair",
"description": "ThemeToggle: light theme label"
},
+ "ZAqGZ6": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "ArticlePage: update date label"
+ },
"ZB/Aw2": {
"defaultMessage": "Partiel inclut seulement l’url de la page, le nombre de visites et la durée.",
"description": "AckeeToggle: tooltip message"
@@ -455,18 +491,18 @@
"defaultMessage": "Vous devriez lire {title}",
"description": "Sharing: subject text"
},
- "b4fdYE": {
- "defaultMessage": "Créé le :",
- "description": "Meta: creation date label"
+ "bfPp0g": {
+ "defaultMessage": "Commentaires :",
+ "description": "Summary: comments label"
+ },
+ "bk0WOp": {
+ "defaultMessage": "Thématiques :",
+ "description": "Summary: thematics label"
},
"bojYF5": {
"defaultMessage": "Accueil",
"description": "Layout: main nav - home link"
},
- "bz53Us": {
- "defaultMessage": "Thématiques :",
- "description": "Meta: thematics label"
- },
"c556Qo": {
"defaultMessage": "Barre latérale",
"description": "PageLayout: accessible name for the sidebar"
@@ -475,6 +511,10 @@
"defaultMessage": "Formulaire des commentaires",
"description": "CommentForm: aria label"
},
+ "f0Z/Po": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "Summary: update date label"
+ },
"fN04AJ": {
"defaultMessage": "<link>Télécharger le CV au format PDF</link>",
"description": "CVPage: download CV in PDF text"
@@ -483,10 +523,6 @@
"defaultMessage": "Échec du chargement.",
"description": "SearchPage: failed to load text"
},
- "fcHeyC": {
- "defaultMessage": "{date} à {time}",
- "description": "Meta: publication date and time"
- },
"fkcTGp": {
"defaultMessage": "Une erreur est survenue :",
"description": "PageLayout: comment form error message"
@@ -499,10 +535,6 @@
"defaultMessage": "Il a été approuvé.",
"description": "PageLayout: comment approved."
},
- "gJNaBD": {
- "defaultMessage": "Sujets :",
- "description": "Meta: topics label"
- },
"gPfT/K": {
"defaultMessage": "Réglages",
"description": "SettingsModal: title"
@@ -523,6 +555,14 @@
"defaultMessage": "{count} secondes",
"description": "useReadingTime: seconds count"
},
+ "iDIKb7": {
+ "defaultMessage": "Dépôts :",
+ "description": "ProjectsPage: repositories label"
+ },
+ "iv3Ex1": {
+ "defaultMessage": "{postsCount, plural, =0 {0 article} one {# article} other {# articles}}",
+ "description": "ThematicPage: posts count meta"
+ },
"j5k9Fe": {
"defaultMessage": "Accueil",
"description": "Breadcrumb: home label"
@@ -531,14 +571,18 @@
"defaultMessage": "Linux",
"description": "HomePage: link to Linux thematic"
},
- "jTVIh8": {
- "defaultMessage": "Commentaires :",
- "description": "Meta: comments label"
+ "kNBXyK": {
+ "defaultMessage": "Total :",
+ "description": "Page: total label"
},
"kzIYoQ": {
"defaultMessage": "Laisser un commentaire",
"description": "PageLayout: comment form title"
},
+ "lHkta9": {
+ "defaultMessage": "Total :",
+ "description": "ThematicPage: total label"
+ },
"lKhTGM": {
"defaultMessage": "Utilisez Ctrl+c pour copier",
"description": "usePrism: copy button error text"
@@ -575,14 +619,14 @@
"defaultMessage": "Pied de page",
"description": "SiteFooter: an accessible name for the footer nav"
},
+ "pT5nHk": {
+ "defaultMessage": "Publié le :",
+ "description": "HomePage: publication date label"
+ },
"pWKyyR": {
"defaultMessage": "Arrêt",
"description": "MotionToggle: deactivate reduce motion label"
},
- "pWTj2W": {
- "defaultMessage": "Popularité :",
- "description": "Meta: popularity label"
- },
"pg26sn": {
"defaultMessage": "Découvrez les résultats de recherche pour {query} sur {websiteName}.",
"description": "SearchPage: SEO - Meta description"
@@ -595,6 +639,10 @@
"defaultMessage": "Projets",
"description": "Layout: main nav - projects link"
},
+ "r/6HOI": {
+ "defaultMessage": "Écrit par :",
+ "description": "Summary: author label"
+ },
"s1i43J": {
"defaultMessage": "{minutesCount} minutes",
"description": "useReadingTime: rounded minutes count"
@@ -619,18 +667,22 @@
"defaultMessage": "Me contacter",
"description": "HomePage: contact button text"
},
+ "soj7do": {
+ "defaultMessage": "Publié le :",
+ "description": "Comment: publication date label"
+ },
"suXOBu": {
"defaultMessage": "Thème :",
"description": "ThemeToggle: theme label"
},
+ "tBX4mb": {
+ "defaultMessage": "Total :",
+ "description": "TopicPage: total label"
+ },
"tIZYpD": {
"defaultMessage": "Partiel",
"description": "AckeeToggle: partial option name"
},
- "tLC7bh": {
- "defaultMessage": "Mis à jour le :",
- "description": "Meta: update date label"
- },
"tMuNTy": {
"defaultMessage": "{websiteName} est intégrateur web / développeur front-end en France. Il code et il écrit essentiellement à propos de développement web et du libre.",
"description": "HomePage: SEO - Meta description"
@@ -639,10 +691,18 @@
"defaultMessage": "Thème clair",
"description": "PrismThemeToggle: light theme label"
},
+ "tyzdql": {
+ "defaultMessage": "Temps de lecture :",
+ "description": "Summary: reading time label"
+ },
"u41qSk": {
"defaultMessage": "Site web :",
"description": "CommentForm: website label"
},
+ "uAL4iW": {
+ "defaultMessage": "{postsCount, plural, =0 {0 article} one {# article} other {# articles}}",
+ "description": "TopicPage: posts count meta"
+ },
"uaqd5F": {
"defaultMessage": "Charger plus d’articles ?",
"description": "PostsList: load more button"
@@ -667,6 +727,14 @@
"defaultMessage": "Libre",
"description": "HomePage: link to free thematic"
},
+ "wQrvgw": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "ProjectsPage: update date label"
+ },
+ "wVFA4m": {
+ "defaultMessage": "Créé le :",
+ "description": "ProjectsPage: creation date label"
+ },
"xYNeKX": {
"defaultMessage": "Formulaire des réglages",
"description": "SettingsModal: an accessible form name"
@@ -687,10 +755,18 @@
"defaultMessage": "Vous êtes ici :",
"description": "Pagination: current page indication"
},
+ "yIZ+AC": {
+ "defaultMessage": "Sujets :",
+ "description": "Summary: topics label"
+ },
"yN5P+m": {
"defaultMessage": "Message :",
"description": "ContactForm: message label"
},
+ "ye/vlA": {
+ "defaultMessage": "{commentsCount, plural, =0 {0 commentaire} one {# commentaire} other {# commentaires}}<a11y> à propos de {title}</a11y>",
+ "description": "Summary: comments count"
+ },
"yfgMcl": {
"defaultMessage": "Introduction :",
"description": "Sharing: email content prefix"
@@ -702,5 +778,9 @@
"zbzlb1": {
"defaultMessage": "Page {number}",
"description": "BlogPage: page number"
+ },
+ "zoifQd": {
+ "defaultMessage": "Site officiel :",
+ "description": "TopicPage: official website label"
}
}
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index acb80b2..bce493b 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -12,9 +12,9 @@ import {
getLayout,
Link,
PageLayout,
- type PageLayoutProps,
Sharing,
Spinner,
+ type MetaItemData,
} from '../../components';
import {
getAllArticlesSlugs,
@@ -26,6 +26,7 @@ import type { Article, NextPageWithLayout, SingleComment } from '../../types';
import { ROUTES } from '../../utils/constants';
import {
getBlogSchema,
+ getFormattedDate,
getSchemaJson,
getSinglePageSchema,
getWebPageSchema,
@@ -82,37 +83,113 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
const { content, id, intro, meta, title } = article;
const { author, commentsCount, cover, dates, seo, thematics, topics } = meta;
- const headerMeta: PageLayoutProps['headerMeta'] = {
- author: author?.name,
- publication: { date: dates.publication },
- update:
- dates.update && dates.publication !== dates.update
- ? { date: dates.update }
- : undefined,
- readingTime,
- thematics: thematics?.map((thematic) => (
- <Link key={thematic.id} href={thematic.url}>
- {thematic.name}
- </Link>
- )),
+ /**
+ * 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 headerMeta: (MetaItemData | undefined)[] = [
+ author
+ ? {
+ id: 'author',
+ label: intl.formatMessage({
+ defaultMessage: 'Written by:',
+ description: 'ArticlePage: author label',
+ id: 'MJbZfX',
+ }),
+ value: author.name,
+ }
+ : undefined,
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'ArticlePage: publication date label',
+ id: 'RecdwX',
+ }),
+ value: getDate(dates.publication),
+ },
+ dates.update && dates.publication !== dates.update
+ ? {
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'ArticlePage: update date label',
+ id: 'ZAqGZ6',
+ }),
+ value: getDate(dates.update),
+ }
+ : undefined,
+ {
+ id: 'reading-time',
+ label: intl.formatMessage({
+ defaultMessage: 'Reading time:',
+ description: 'ArticlePage: reading time label',
+ id: 'Gw7X3x',
+ }),
+ value: readingTime,
+ },
+ thematics
+ ? {
+ id: 'thematics',
+ label: intl.formatMessage({
+ defaultMessage: 'Thematics:',
+ description: 'ArticlePage: thematics meta label',
+ id: 'CvOqoh',
+ }),
+ value: thematics.map((thematic) => {
+ return {
+ id: `thematic-${thematic.id}`,
+ value: (
+ <Link key={thematic.id} href={thematic.url}>
+ {thematic.name}
+ </Link>
+ ),
+ };
+ }),
+ }
+ : undefined,
+ ];
+ const filteredHeaderMeta = headerMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
+
const footerMetaLabel = intl.formatMessage({
defaultMessage: 'Read more articles about:',
description: 'ArticlePage: footer topics list label',
id: '50xc4o',
});
- const footerMeta: PageLayoutProps['footerMeta'] = {
- custom: topics && {
- label: footerMetaLabel,
- value: topics.map((topic) => (
- <ButtonLink className={styles.btn} key={topic.id} to={topic.url}>
- {topic.logo ? <NextImage {...topic.logo} /> : null} {topic.name}
- </ButtonLink>
- )),
- },
- };
+ const footerMeta: MetaItemData[] = topics
+ ? [
+ {
+ id: 'more-about',
+ label: footerMetaLabel,
+ value: topics.map((topic) => {
+ return {
+ id: `topic--${topic.id}`,
+ value: (
+ <ButtonLink
+ className={styles.btn}
+ key={topic.id}
+ to={topic.url}
+ >
+ {topic.logo ? <NextImage {...topic.logo} /> : null}{' '}
+ {topic.name}
+ </ButtonLink>
+ ),
+ };
+ }),
+ },
+ ]
+ : [];
const webpageSchema = getWebPageSchema({
description: intro,
@@ -208,7 +285,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
breadcrumbSchema={breadcrumbSchema}
comments={commentsData}
footerMeta={footerMeta}
- headerMeta={headerMeta}
+ headerMeta={filteredHeaderMeta}
id={id as number}
intro={intro}
title={title}
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index 0241a5d..5c64e6d 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -9,6 +9,7 @@ import {
getLayout,
Heading,
LinksListWidget,
+ type MetaItemData,
Notice,
PageLayout,
PostsList,
@@ -134,6 +135,28 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
});
const postsListBaseUrl = `${ROUTES.BLOG}/page/`;
+ const headerMeta: MetaItemData[] = totalArticles
+ ? [
+ {
+ id: 'posts-count',
+ label: intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'Page: total label',
+ id: 'kNBXyK',
+ }),
+ value: intl.formatMessage(
+ {
+ defaultMessage:
+ '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'Page: posts count meta',
+ id: 'RvGb2c',
+ },
+ { postsCount: totalArticles }
+ ),
+ },
+ ]
+ : [];
+
return (
<>
<Head>
@@ -157,7 +180,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
title={title}
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
- headerMeta={{ total: totalArticles }}
+ headerMeta={headerMeta}
widgets={[
<LinksListWidget
heading={
diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx
index 15d7245..58cf7b9 100644
--- a/src/pages/blog/page/[number].tsx
+++ b/src/pages/blog/page/[number].tsx
@@ -9,6 +9,7 @@ import {
getLayout,
Heading,
LinksListWidget,
+ type MetaItemData,
PageLayout,
PostsList,
} from '../../../components';
@@ -132,6 +133,28 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
});
const postsListBaseUrl = `${ROUTES.BLOG}/page/`;
+ const headerMeta: MetaItemData[] = totalArticles
+ ? [
+ {
+ id: 'posts-count',
+ label: intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'Page: total label',
+ id: 'kNBXyK',
+ }),
+ value: intl.formatMessage(
+ {
+ defaultMessage:
+ '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'Page: posts count meta',
+ id: 'RvGb2c',
+ },
+ { postsCount: totalArticles }
+ ),
+ },
+ ]
+ : [];
+
return (
<>
<Head>
@@ -155,7 +178,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
title={pageTitleWithPageNumber}
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
- headerMeta={{ total: totalArticles }}
+ headerMeta={headerMeta}
widgets={[
<LinksListWidget
heading={
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 206c7f5..652b913 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -18,14 +18,15 @@ import {
List,
PageLayout,
SocialMedia,
- type MetaData,
ListItem,
+ type MetaItemData,
} from '../components';
import CVContent, { data, meta } from '../content/pages/cv.mdx';
import styles from '../styles/pages/cv.module.scss';
import type { NextPageWithLayout } from '../types';
import { PERSONAL_LINKS, ROUTES } from '../utils/constants';
import {
+ getFormattedDate,
getSchemaJson,
getSinglePageSchema,
getWebPageSchema,
@@ -152,16 +153,43 @@ const CVPage: NextPageWithLayout = () => {
id: '+Dre5J',
});
- const headerMeta: MetaData = {
- publication: {
- date: dates.publication,
+ /**
+ * 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 headerMeta: (MetaItemData | undefined)[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Page: publication date label',
+ id: '4QbTDq',
+ }),
+ value: getDate(dates.publication),
},
- update: dates.update
+ dates.update
? {
- date: dates.update,
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'Page: update date label',
+ id: 'Ez8Qim',
+ }),
+ value: getDate(dates.update),
}
: undefined,
- };
+ ];
+ const filteredMeta = headerMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
const { website } = useSettings();
const cvCaption = intl.formatMessage(
@@ -267,7 +295,7 @@ const CVPage: NextPageWithLayout = () => {
<PageLayout
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
- headerMeta={headerMeta}
+ headerMeta={filteredMeta}
intro={intro}
title={title}
widgets={widgets}
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index d94160f..cdc51c5 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable max-statements */
import type { MDXComponents } from 'mdx/types';
import type { GetStaticProps } from 'next';
import Head from 'next/head';
@@ -26,7 +27,11 @@ import { getArticlesCard } from '../services/graphql';
import styles from '../styles/pages/home.module.scss';
import type { ArticleCard, NextPageWithLayout } from '../types';
import { PERSONAL_LINKS, ROUTES } from '../utils/constants';
-import { getSchemaJson, getWebPageSchema } from '../utils/helpers';
+import {
+ getFormattedDate,
+ getSchemaJson,
+ getWebPageSchema,
+} from '../utils/helpers';
import { loadTranslation, type Messages } from '../utils/helpers/server';
import { useBreadcrumb, useSettings } from '../utils/hooks';
@@ -279,6 +284,11 @@ type HomeProps = {
*/
const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
const intl = useIntl();
+ const publicationDate = intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'HomePage: publication date label',
+ id: 'pT5nHk',
+ });
const { schema: breadcrumbSchema } = useBreadcrumb({
title: '',
url: `/`,
@@ -291,10 +301,22 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
*/
const getRecentPosts = (): JSX.Element => {
const posts: CardsListItem[] = recentPosts.map((post) => {
+ const isoDate = new Date(`${post.dates.publication}`).toISOString();
+
return {
cover: post.cover,
id: post.slug,
- meta: { publication: { date: post.dates.publication } },
+ meta: [
+ {
+ id: 'publication-date',
+ label: publicationDate,
+ value: (
+ <time dateTime={isoDate}>
+ {getFormattedDate(post.dates.publication)}
+ </time>
+ ),
+ },
+ ],
title: post.title,
url: `${ROUTES.ARTICLE}/${post.slug}`,
};
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index 810d9ec..25c2dd9 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -1,20 +1,23 @@
+/* eslint-disable max-statements */
import type { MDXComponents } from 'mdx/types';
import type { GetStaticProps } from 'next';
import Head from 'next/head';
import NextImage, { type ImageProps as NextImageProps } from 'next/image';
import { useRouter } from 'next/router';
import Script from 'next/script';
+import { useIntl } from 'react-intl';
import {
getLayout,
Link,
PageLayout,
- type MetaData,
Figure,
+ type MetaItemData,
} from '../components';
import LegalNoticeContent, { meta } from '../content/pages/legal-notice.mdx';
import type { NextPageWithLayout } from '../types';
import { ROUTES } from '../utils/constants';
import {
+ getFormattedDate,
getSchemaJson,
getSinglePageSchema,
getWebPageSchema,
@@ -37,22 +40,50 @@ const components: MDXComponents = {
* Legal Notice page.
*/
const LegalNoticePage: NextPageWithLayout = () => {
+ const intl = useIntl();
const { dates, intro, seo, title } = meta;
const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
title,
url: ROUTES.LEGAL_NOTICE,
});
- const headerMeta: MetaData = {
- publication: {
- date: dates.publication,
+ /**
+ * 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 headerMeta: (MetaItemData | undefined)[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Page: publication date label',
+ id: '4QbTDq',
+ }),
+ value: getDate(dates.publication),
},
- update: dates.update
+ dates.update
? {
- date: dates.update,
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'Page: update date label',
+ id: 'Ez8Qim',
+ }),
+ value: getDate(dates.update),
}
: undefined,
- };
+ ];
+ const filteredMeta = headerMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
const { website } = useSettings();
const { asPath } = useRouter();
@@ -82,7 +113,7 @@ const LegalNoticePage: NextPageWithLayout = () => {
<PageLayout
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
- headerMeta={headerMeta}
+ headerMeta={filteredMeta}
intro={intro}
title={title}
withToC={true}
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index 0b94a4e..6ef3df5 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -14,21 +14,22 @@ import {
getLayout,
Link,
Overview,
- type OverviewMeta,
PageLayout,
Sharing,
SocialLink,
Spinner,
- type MetaData,
Heading,
List,
ListItem,
Figure,
+ type MetaItemData,
+ type MetaValues,
} from '../../components';
import styles from '../../styles/pages/project.module.scss';
import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types';
import { ROUTES } from '../../utils/constants';
import {
+ getFormattedDate,
getSchemaJson,
getSinglePageSchema,
getWebPageSchema,
@@ -166,22 +167,52 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
url: `${website.url}${asPath}`,
};
- const headerMeta: MetaData = {
- publication: { date: dates.publication },
- update:
- dates.update && dates.update !== dates.publication
- ? { date: dates.update }
- : undefined,
+ /**
+ * 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 headerMeta: (MetaItemData | undefined)[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'ProjectsPage: publication date label',
+ id: 'HxZvY4',
+ }),
+ value: getDate(dates.publication),
+ },
+ dates.update && dates.update !== dates.publication
+ ? {
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'ProjectsPage: update date label',
+ id: 'wQrvgw',
+ }),
+ value: getDate(dates.update),
+ }
+ : undefined,
+ ];
+ const filteredHeaderMeta = headerMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
+
/**
* Retrieve the repositories links.
*
* @param {Repos} repositories - A repositories object.
- * @returns {JSX.Element[]} - An array of SocialLink.
+ * @returns {MetaValues[]} - An array of meta values.
*/
- const getReposLinks = (repositories: Repos): JSX.Element[] => {
- const links = [];
+ const getReposLinks = (repositories: Repos): MetaValues[] => {
+ const links: MetaValues[] = [];
const githubLabel = intl.formatMessage({
defaultMessage: 'Github profile',
description: 'ProjectsPage: Github profile link',
@@ -194,22 +225,28 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
});
if (repositories.github)
- links.push(
- <SocialLink
- icon="Github"
- label={githubLabel}
- url={repositories.github}
- />
- );
+ links.push({
+ id: 'github',
+ value: (
+ <SocialLink
+ icon="Github"
+ label={githubLabel}
+ url={repositories.github}
+ />
+ ),
+ });
if (repositories.gitlab)
- links.push(
- <SocialLink
- icon="Gitlab"
- label={gitlabLabel}
- url={repositories.gitlab}
- />
- );
+ links.push({
+ id: 'gitlab',
+ value: (
+ <SocialLink
+ icon="Gitlab"
+ label={gitlabLabel}
+ url={repositories.gitlab}
+ />
+ ),
+ });
return links;
};
@@ -254,14 +291,75 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
);
};
- const overviewData: OverviewMeta = {
- creation: { date: data.created_at },
- update: { date: data.updated_at },
- license,
- popularity: repos?.github && getRepoPopularity(repos.github),
- repositories: repos ? getReposLinks(repos) : undefined,
- technologies,
- };
+ const overviewMeta: (MetaItemData | undefined)[] = [
+ {
+ id: 'creation-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Created on:',
+ description: 'ProjectsPage: creation date label',
+ id: 'wVFA4m',
+ }),
+ value: getDate(data.created_at),
+ },
+ {
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'ProjectsPage: update date label',
+ id: 'wQrvgw',
+ }),
+ value: getDate(data.updated_at),
+ },
+ license
+ ? {
+ id: 'license',
+ label: intl.formatMessage({
+ defaultMessage: 'License:',
+ description: 'ProjectsPage: license label',
+ id: 'VtYzuv',
+ }),
+ value: license,
+ }
+ : undefined,
+ repos?.github
+ ? {
+ id: 'popularity',
+ label: intl.formatMessage({
+ defaultMessage: 'Popularity:',
+ description: 'ProjectsPage: popularity label',
+ id: 'KrNvQi',
+ }),
+ value: getRepoPopularity(repos.github),
+ }
+ : undefined,
+ repos
+ ? {
+ id: 'repositories',
+ label: intl.formatMessage({
+ defaultMessage: 'Repositories:',
+ description: 'ProjectsPage: repositories label',
+ id: 'iDIKb7',
+ }),
+ value: getReposLinks(repos),
+ }
+ : undefined,
+ technologies
+ ? {
+ id: 'technologies',
+ label: intl.formatMessage({
+ defaultMessage: 'Technologies:',
+ description: 'ProjectsPage: technologies label',
+ id: 'RwNZ6p',
+ }),
+ value: technologies.map((techno) => {
+ return { id: techno, value: techno };
+ }),
+ }
+ : undefined,
+ ];
+ const filteredOverviewMeta = overviewMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
const webpageSchema = getWebPageSchema({
description: seo.description,
@@ -306,7 +404,7 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
intro={intro}
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
- headerMeta={headerMeta}
+ headerMeta={filteredHeaderMeta}
withToC={true}
widgets={[
<Sharing
@@ -325,7 +423,7 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
/>,
]}
>
- <Overview cover={cover} meta={overviewData} />
+ <Overview cover={cover} meta={filteredOverviewMeta} />
<ProjectContent components={components} />
</PageLayout>
</>
diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx
index 97963dd..44354ce 100644
--- a/src/pages/projets/index.tsx
+++ b/src/pages/projets/index.tsx
@@ -1,8 +1,10 @@
+/* eslint-disable max-statements */
import type { MDXComponents } from 'mdx/types';
import type { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
+import { useIntl } from 'react-intl';
import {
CardsList,
type CardsListItem,
@@ -44,6 +46,12 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {
title,
url: ROUTES.PROJECTS,
});
+ const intl = useIntl();
+ const metaLabel = intl.formatMessage({
+ defaultMessage: 'Technologies:',
+ description: 'Meta: technologies label',
+ id: 'ADQmDF',
+ });
const items: CardsListItem[] = projects.map(
({ id, meta: projectMeta, slug, title: projectTitle }) => {
@@ -52,7 +60,17 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {
return {
cover,
id: id as string,
- meta: { technologies },
+ meta: technologies?.length
+ ? [
+ {
+ id: 'technologies',
+ label: metaLabel,
+ value: technologies.map((techno) => {
+ return { id: techno, value: techno };
+ }),
+ },
+ ]
+ : [],
tagline,
title: projectTitle,
url: `${ROUTES.PROJECTS}/${slug}`,
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index f47e40c..32312ec 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -9,6 +9,7 @@ import {
getLayout,
Heading,
LinksListWidget,
+ type MetaItemData,
Notice,
PageLayout,
PostsList,
@@ -133,6 +134,28 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({
getTotalArticles(query.s as string)
);
+ const headerMeta: MetaItemData[] = totalArticles
+ ? [
+ {
+ id: 'posts-count',
+ label: intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'Page: total label',
+ id: 'kNBXyK',
+ }),
+ value: intl.formatMessage(
+ {
+ defaultMessage:
+ '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'Page: posts count meta',
+ id: 'RvGb2c',
+ },
+ { postsCount: totalArticles }
+ ),
+ },
+ ]
+ : [];
+
/**
* Load more posts handler.
*/
@@ -181,7 +204,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({
title={title}
breadcrumb={breadcrumbItems}
breadcrumbSchema={breadcrumbSchema}
- headerMeta={{ total: totalArticles }}
+ headerMeta={headerMeta}
widgets={[
<LinksListWidget
heading={
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 899f9e1..cacc972 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -10,9 +10,9 @@ import {
getLayout,
Heading,
LinksListWidget,
+ type MetaItemData,
PageLayout,
PostsList,
- type MetaData,
} from '../../components';
import {
getAllTopicsSlugs,
@@ -24,6 +24,7 @@ import styles from '../../styles/pages/topic.module.scss';
import type { NextPageWithLayout, PageLink, Topic } from '../../types';
import { ROUTES } from '../../utils/constants';
import {
+ getFormattedDate,
getLinksListItems,
getPageLinkFromRawData,
getPostsWithUrl,
@@ -59,13 +60,74 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
url: `${ROUTES.TOPICS}/${slug}`,
});
- const headerMeta: MetaData = {
- publication: { date: dates.publication },
- update: dates.update ? { date: dates.update } : undefined,
- website: officialWebsite,
- total: articles ? articles.length : undefined,
+ /**
+ * 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 headerMeta: (MetaItemData | undefined)[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'TopicPage: publication date label',
+ id: 'KV+NMZ',
+ }),
+ value: getDate(dates.publication),
+ },
+ dates.update
+ ? {
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'TopicPage: update date label',
+ id: '9DfuHk',
+ }),
+ value: getDate(dates.update),
+ }
+ : undefined,
+ officialWebsite
+ ? {
+ id: 'website',
+ label: intl.formatMessage({
+ defaultMessage: 'Official website:',
+ description: 'TopicPage: official website label',
+ id: 'zoifQd',
+ }),
+ value: officialWebsite,
+ }
+ : undefined,
+ articles?.length
+ ? {
+ id: 'total',
+ label: intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'TopicPage: total label',
+ id: 'tBX4mb',
+ }),
+ value: intl.formatMessage(
+ {
+ defaultMessage:
+ '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'TopicPage: posts count meta',
+ id: 'uAL4iW',
+ },
+ { postsCount: articles.length }
+ ),
+ }
+ : undefined,
+ ];
+ const filteredMeta = headerMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
+
const { website } = useSettings();
const { asPath } = useRouter();
const webpageSchema = getWebPageSchema({
@@ -132,7 +194,7 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({
breadcrumbSchema={breadcrumbSchema}
title={getPageHeading()}
intro={intro}
- headerMeta={headerMeta}
+ headerMeta={filteredMeta}
widgets={
thematics
? [
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index 95b4780..a5badf3 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -9,9 +9,9 @@ import {
getLayout,
Heading,
LinksListWidget,
+ type MetaItemData,
PageLayout,
PostsList,
- type MetaData,
} from '../../components';
import {
getAllThematicsSlugs,
@@ -22,6 +22,7 @@ import {
import type { NextPageWithLayout, PageLink, Thematic } from '../../types';
import { ROUTES } from '../../utils/constants';
import {
+ getFormattedDate,
getLinksListItems,
getPageLinkFromRawData,
getPostsWithUrl,
@@ -50,12 +51,63 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
url: `${ROUTES.THEMATICS.INDEX}/${slug}`,
});
- const headerMeta: MetaData = {
- publication: { date: dates.publication },
- update: dates.update ? { date: dates.update } : undefined,
- total: articles ? articles.length : undefined,
+ /**
+ * 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 headerMeta: (MetaItemData | undefined)[] = [
+ {
+ id: 'publication-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'ThematicPage: publication date label',
+ id: 'UTGhUU',
+ }),
+ value: getDate(dates.publication),
+ },
+ dates.update
+ ? {
+ id: 'update-date',
+ label: intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'ThematicPage: update date label',
+ id: '24FIsG',
+ }),
+ value: getDate(dates.update),
+ }
+ : undefined,
+ articles
+ ? {
+ id: 'total',
+ label: intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'ThematicPage: total label',
+ id: 'lHkta9',
+ }),
+ value: intl.formatMessage(
+ {
+ defaultMessage:
+ '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'ThematicPage: posts count meta',
+ id: 'iv3Ex1',
+ },
+ { postsCount: articles.length }
+ ),
+ }
+ : undefined,
+ ];
+ const filteredMeta = headerMeta.filter(
+ (item): item is MetaItemData => !!item
+ );
+
const { website } = useSettings();
const { asPath } = useRouter();
const webpageSchema = getWebPageSchema({
@@ -114,7 +166,7 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
breadcrumbSchema={breadcrumbSchema}
title={title}
intro={intro}
- headerMeta={headerMeta}
+ headerMeta={filteredMeta}
widgets={
topics
? [
diff --git a/src/styles/abstracts/placeholders/_lists.scss b/src/styles/abstracts/placeholders/_lists.scss
index 780fd21..2200336 100644
--- a/src/styles/abstracts/placeholders/_lists.scss
+++ b/src/styles/abstracts/placeholders/_lists.scss
@@ -75,5 +75,5 @@
%description {
margin: 0;
- word-break: break-all;
+ overflow-wrap: break-word;
}
diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss
index 068826f..d2e7822 100644
--- a/src/styles/pages/article.module.scss
+++ b/src/styles/pages/article.module.scss
@@ -12,9 +12,8 @@
margin-right: var(--spacing-2xs);
padding: var(--spacing-2xs) var(--spacing-xs);
- figure {
+ img {
max-width: fun.convert-px(22);
- margin-right: var(--spacing-2xs);
}
}