diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/mdx.tsx | 13 | ||||
| -rw-r--r-- | src/components/molecules/grid/grid-item.tsx | 15 | ||||
| -rw-r--r-- | src/components/molecules/grid/grid.module.scss | 9 | ||||
| -rw-r--r-- | src/components/molecules/grid/grid.stories.tsx | 224 | ||||
| -rw-r--r-- | src/components/molecules/grid/grid.test.tsx | 73 | ||||
| -rw-r--r-- | src/components/molecules/grid/grid.tsx | 23 | ||||
| -rw-r--r-- | src/components/molecules/grid/index.ts | 1 | ||||
| -rw-r--r-- | src/pages/index.tsx | 42 | ||||
| -rw-r--r-- | src/pages/projets/index.tsx | 113 | 
9 files changed, 309 insertions, 204 deletions
| diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx index f11dda5..9f0a4a5 100644 --- a/src/components/mdx.tsx +++ b/src/components/mdx.tsx @@ -2,7 +2,7 @@ import type { MDXComponents } from 'mdx/types';  import NextImage from 'next/image';  import type { AnchorHTMLAttributes, ImgHTMLAttributes, ReactNode } from 'react';  import { Figure, Heading, Link, List, ListItem } from './atoms'; -import { Code, Grid } from './molecules'; +import { Code, Grid, GridItem } from './molecules';  const Anchor = ({    children = '', @@ -45,16 +45,15 @@ const Img = ({    return <img {...props} alt={alt} height={height} src={src} width={width} />;  }; -const Gallery = ({ children }: { children: ReactNode[] }) => ( +const Gallery = ({ children }: { children: ReactNode }) => (    <Grid      // eslint-disable-next-line react/jsx-no-literals      gap="sm" -    items={children.map((child, index) => { -      return { id: `${index}`, item: child }; -    })}      // eslint-disable-next-line react/jsx-no-literals      sizeMin="250px" -  /> +  > +    {children} +  </Grid>  );  export const mdxComponents: MDXComponents = { @@ -63,6 +62,8 @@ export const mdxComponents: MDXComponents = {    figure: ({ ref, ...props }) => <Figure {...props} />,    Figure,    Gallery, +  Grid, +  GridItem,    h1: ({ ref, ...props }) => <Heading {...props} level={1} />,    h2: ({ ref, ...props }) => <Heading {...props} level={2} />,    h3: ({ ref, ...props }) => <Heading {...props} level={3} />, diff --git a/src/components/molecules/grid/grid-item.tsx b/src/components/molecules/grid/grid-item.tsx new file mode 100644 index 0000000..0592cec --- /dev/null +++ b/src/components/molecules/grid/grid-item.tsx @@ -0,0 +1,15 @@ +import { type ForwardRefRenderFunction, forwardRef } from 'react'; +import { ListItem, type ListItemProps } from '../../atoms'; + +export type GridItemProps = ListItemProps; + +const GridItemWithRef: ForwardRefRenderFunction< +  HTMLLIElement, +  GridItemProps +> = ({ children, ...props }, ref) => ( +  <ListItem {...props} ref={ref}> +    {children} +  </ListItem> +); + +export const GridItem = forwardRef(GridItemWithRef); diff --git a/src/components/molecules/grid/grid.module.scss b/src/components/molecules/grid/grid.module.scss index e253a89..f13af30 100644 --- a/src/components/molecules/grid/grid.module.scss +++ b/src/components/molecules/grid/grid.module.scss @@ -30,12 +30,3 @@      );    }  } - -.item { -  display: flex; -  flex-flow: row wrap; - -  > * { -    flex: 1; -  } -} diff --git a/src/components/molecules/grid/grid.stories.tsx b/src/components/molecules/grid/grid.stories.tsx index ce3ee2b..4e12af4 100644 --- a/src/components/molecules/grid/grid.stories.tsx +++ b/src/components/molecules/grid/grid.stories.tsx @@ -1,6 +1,6 @@  import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import type { FC, ReactNode } from 'react';  import { Grid } from './grid'; +import { GridItem } from './grid-item';  export default {    title: 'Molecules/Grid', @@ -19,107 +19,185 @@ export default {  const Template: ComponentStory<typeof Grid> = (args) => <Grid {...args} />; -type ItemProps = { -  children: ReactNode; -}; - -const Item: FC<ItemProps> = ({ children }) => ( -  <div style={{ border: '1px solid #000', padding: '1rem' }}>{children}</div> -); -  export const Default = Template.bind({});  Default.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -    { id: 'item-5', item: <Item>Item 5</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 5 +      </GridItem> +    </> +  ),  };  export const OneColumn = Template.bind({});  OneColumn.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +    </> +  ),    col: 1,    gap: 'sm',  };  export const TwoColumns = Template.bind({});  TwoColumns.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +    </> +  ),    col: 2,    gap: 'sm',  };  export const ThreeColumns = Template.bind({});  ThreeColumns.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +    </> +  ),    col: 3,    gap: 'sm',  };  export const FixedSize = Template.bind({});  FixedSize.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -    { id: 'item-5', item: <Item>Item 5</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 5 +      </GridItem> +    </> +  ),    size: '300px',    gap: 'sm',  };  export const MaxSize = Template.bind({});  MaxSize.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -    { id: 'item-5', item: <Item>Item 5</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 5 +      </GridItem> +    </> +  ),    sizeMax: '300px',    gap: 'sm',  };  export const MinSize = Template.bind({});  MinSize.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -    { id: 'item-5', item: <Item>Item 5</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 5 +      </GridItem> +    </> +  ),    sizeMin: '100px',    gap: 'sm',  };  export const MinAndMaxSize = Template.bind({});  MinAndMaxSize.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -    { id: 'item-5', item: <Item>Item 5</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 5 +      </GridItem> +    </> +  ),    sizeMax: '300px',    sizeMin: '100px',    gap: 'sm', @@ -127,13 +205,25 @@ MinAndMaxSize.args = {  export const Fill = Template.bind({});  Fill.args = { -  items: [ -    { id: 'item-1', item: <Item>Item 1</Item> }, -    { id: 'item-2', item: <Item>Item 2</Item> }, -    { id: 'item-3', item: <Item>Item 3</Item> }, -    { id: 'item-4', item: <Item>Item 4</Item> }, -    { id: 'item-5', item: <Item>Item 5</Item> }, -  ], +  children: ( +    <> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 1 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 2 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 3 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 4 +      </GridItem> +      <GridItem style={{ border: '1px solid #000', padding: '1rem' }}> +        Item 5 +      </GridItem> +    </> +  ),    col: 'auto-fill',    sizeMin: '100px',    gap: 'sm', diff --git a/src/components/molecules/grid/grid.test.tsx b/src/components/molecules/grid/grid.test.tsx index 212bdc4..e69610d 100644 --- a/src/components/molecules/grid/grid.test.tsx +++ b/src/components/molecules/grid/grid.test.tsx @@ -1,18 +1,25 @@  import { describe, expect, it } from '@jest/globals';  import { render, screen as rtlScreen } from '@testing-library/react'; -import { Grid, type GridItem } from './grid'; - -const items: GridItem[] = [ -  { id: 'item-1', item: 'Item 1' }, -  { id: 'item-2', item: 'Item 2' }, -  { id: 'item-3', item: 'Item 3' }, -  { id: 'item-4', item: 'Item 4' }, -  { id: 'item-5', item: 'Item 5' }, +import { Grid } from './grid'; +import { GridItem } from './grid-item'; + +const items = [ +  { id: 'item-1', contents: 'Item 1' }, +  { id: 'item-2', contents: 'Item 2' }, +  { id: 'item-3', contents: 'Item 3' }, +  { id: 'item-4', contents: 'Item 4' }, +  { id: 'item-5', contents: 'Item 5' },  ];  describe('Grid', () => {    it('render a list of items as grid', () => { -    render(<Grid items={items} />); +    render( +      <Grid> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length);    }); @@ -20,7 +27,13 @@ describe('Grid', () => {    it('can render a list of items with fixed size', () => {      const size = '200px'; -    render(<Grid items={items} size={size} />); +    render( +      <Grid size={size}> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--has-fixed-size');      expect(rtlScreen.getByRole('list')).toHaveStyle({ '--size': size }); @@ -29,7 +42,13 @@ describe('Grid', () => {    it('can render a list of items with min size', () => {      const size = '200px'; -    render(<Grid items={items} sizeMin={size} />); +    render( +      <Grid sizeMin={size}> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--has-min-size');      expect(rtlScreen.getByRole('list')).toHaveStyle({ '--size-min': size }); @@ -38,7 +57,13 @@ describe('Grid', () => {    it('can render a list of items with max size', () => {      const size = '200px'; -    render(<Grid items={items} sizeMax={size} />); +    render( +      <Grid sizeMax={size}> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getByRole('list')).toHaveStyle({ '--size-max': size });    }); @@ -46,7 +71,13 @@ describe('Grid', () => {    it('can render a list of items with a custom gap', () => {      const gap = 'md'; -    render(<Grid items={items} gap={gap} />); +    render( +      <Grid gap={gap}> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getByRole('list')).toHaveStyle({        '--gap': `var(--spacing-${gap})`, @@ -56,13 +87,25 @@ describe('Grid', () => {    it('can render a list of items with an explicit number of columns', () => {      const col = 4; -    render(<Grid col={col} items={items} />); +    render( +      <Grid col={col}> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getByRole('list')).toHaveStyle(`--col: ${col}`);    });    it('can render a centered list of items', () => { -    render(<Grid isCentered items={items} />); +    render( +      <Grid isCentered> +        {items.map((item) => ( +          <GridItem key={item.id}>{item.contents}</GridItem> +        ))} +      </Grid> +    );      expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--is-centered');    }); diff --git a/src/components/molecules/grid/grid.tsx b/src/components/molecules/grid/grid.tsx index ca920f8..3d0ecf1 100644 --- a/src/components/molecules/grid/grid.tsx +++ b/src/components/molecules/grid/grid.tsx @@ -5,19 +5,18 @@ import {    type CSSProperties,  } from 'react';  import type { Spacing } from '../../../types'; -import { List, ListItem, type ListProps } from '../../atoms'; +import { List, type ListProps } from '../../atoms';  import styles from './grid.module.scss'; -export type GridItem = { -  id: string; -  item: ReactNode; -}; -  export type GridProps<T extends boolean> = Omit<    ListProps<T, false>,    'children' | 'hideMarker' | 'isHierarchical' | 'isInline' | 'spacing'  > & {    /** +   * The grid items. +   */ +  children: ReactNode; +  /**     * Control the number of column.     *     * @default 'auto-fit' @@ -36,10 +35,6 @@ export type GridProps<T extends boolean> = Omit<     */    isCentered?: boolean;    /** -   * The grid items. -   */ -  items: GridItem[]; -  /**     * Define a fixed size for each item.     *     * You should either use `size` or `sizeMax`/`sizeMin` not both. @@ -67,11 +62,11 @@ export type GridProps<T extends boolean> = Omit<  const GridWithRef = <T extends boolean>(    { +    children,      className = '',      col = 'auto-fit',      gap,      isCentered = false, -    items,      size,      sizeMax,      sizeMin, @@ -104,11 +99,7 @@ const GridWithRef = <T extends boolean>(        ref={ref}        style={gridStyles}      > -      {items.map(({ id, item }) => ( -        <ListItem className={styles.item} key={id}> -          {item} -        </ListItem> -      ))} +      {children}      </List>    );  }; diff --git a/src/components/molecules/grid/index.ts b/src/components/molecules/grid/index.ts index d24d1bd..d657dcb 100644 --- a/src/components/molecules/grid/index.ts +++ b/src/components/molecules/grid/index.ts @@ -1 +1,2 @@  export * from './grid'; +export * from './grid-item'; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 32c2e7f..b8f754b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -15,7 +15,6 @@ import {    CardTitle,    getLayout,    Grid, -  type GridItem,    Icon,    List,    ListItem, @@ -151,12 +150,11 @@ const LibreLinks: FC = () => {   */  const ShaarliLink: FC = () => {    const intl = useIntl(); -  const shaarliUrl = PERSONAL_LINKS.SHAARLI;    return (      <List className={styles.list} hideMarker isInline spacing="sm">        <ListItem> -        <ButtonLink isExternal to={shaarliUrl}> +        <ButtonLink isExternal to={PERSONAL_LINKS.SHAARLI}>            {intl.formatMessage({              defaultMessage: 'Shaarli',              description: 'HomePage: link to Shaarli', @@ -202,15 +200,10 @@ const MoreLinks: FC = () => {    );  }; -const StyledGrid = ({ children }: { children: ReactNode[] }) => ( -  <Grid -    className={styles.columns} -    gap="sm" -    items={children.map((child, index) => { -      return { id: `${index}`, item: child }; -    })} -    sizeMin="250px" -  /> +const StyledGrid = ({ children }: { children: ReactNode }) => ( +  <Grid className={styles.columns} gap="sm" sizeMin="250px"> +    {children} +  </Grid>  );  /** @@ -256,9 +249,11 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {     * @returns {JSX.Element} - The cards list.     */    const getRecentPosts = (): JSX.Element => { -    const posts: GridItem[] = recentPosts.map((post) => { -      return { -        item: ( +    const listClass = `${styles.list} ${styles['list--cards']}`; + +    return ( +      <Grid className={listClass} gap="sm" isCentered sizeMax="25ch"> +        {recentPosts.map((post) => (            <Card              cover={                post.cover ? ( @@ -270,6 +265,7 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {                  </CardCover>                ) : undefined              } +            key={post.id}              meta={                <CardMeta isCentered>                  <MetaItem @@ -288,20 +284,8 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {              </CardHeader>              <CardFooter />            </Card> -        ), -        id: `${post.id}`, -      }; -    }); -    const listClass = `${styles.list} ${styles['list--cards']}`; - -    return ( -      <Grid -        className={listClass} -        gap="sm" -        isCentered -        items={posts} -        sizeMax="25ch" -      /> +        ))} +      </Grid>      );    }; diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx index fc6eb5f..00c5a70 100644 --- a/src/pages/projets/index.tsx +++ b/src/pages/projets/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable max-statements */  import type { GetStaticProps } from 'next';  import Head from 'next/head';  import NextImage from 'next/image'; @@ -14,7 +13,6 @@ import {    CardTitle,    getLayout,    Grid, -  type GridItem,    MetaList,    MetaItem,    Page, @@ -59,59 +57,6 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {      description: 'Meta: technologies label',      id: 'ADQmDF',    }); - -  const items: GridItem[] = projects.map( -    ({ id, meta: projectMeta, slug, title: projectTitle }) => { -      const { cover, tagline, technologies } = projectMeta; -      const figureLabel = intl.formatMessage( -        { -          defaultMessage: '{title} cover', -          description: 'ProjectsPage: figure (cover) accessible name', -          id: 'FdF33B', -        }, -        { title: projectTitle } -      ); - -      return { -        item: ( -          <Card -            cover={ -              cover ? ( -                <CardCover aria-label={figureLabel} hasBorders> -                  <NextImage {...cover} /> -                </CardCover> -              ) : undefined -            } -            meta={ -              technologies ? ( -                <MetaList isCentered> -                  <MetaItem -                    hasBorderedValues -                    hasInlinedValues -                    isCentered -                    label={metaLabel} -                    value={technologies.map((techno) => { -                      return { id: techno, value: techno }; -                    })} -                  /> -                </MetaList> -              ) : undefined -            } -            isCentered -            linkTo={`${ROUTES.PROJECTS}/${slug}`} -          > -            <CardHeader> -              <CardTitle>{projectTitle}</CardTitle> -            </CardHeader> -            <CardBody>{tagline}</CardBody> -            <CardFooter /> -          </Card> -        ), -        id: `${id}`, -      }; -    } -  ); -    const { asPath } = useRouter();    const webpageSchema = getWebPageSchema({      description: seo.description, @@ -165,13 +110,57 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {          intro={<PageContent components={mdxComponents} />}        />        <PageBody className={styles.body}> -        <Grid -          className={styles.list} -          gap="sm" -          isCentered -          items={items} -          sizeMax="30ch" -        /> +        <Grid className={styles.list} gap="sm" isCentered sizeMax="30ch"> +          {projects.map( +            ({ id, meta: projectMeta, slug, title: projectTitle }) => { +              const { cover, tagline, technologies } = projectMeta; +              const figureLabel = intl.formatMessage( +                { +                  defaultMessage: '{title} cover', +                  description: 'ProjectsPage: figure (cover) accessible name', +                  id: 'FdF33B', +                }, +                { title: projectTitle } +              ); + +              return ( +                <Card +                  cover={ +                    cover ? ( +                      <CardCover aria-label={figureLabel} hasBorders> +                        <NextImage {...cover} /> +                      </CardCover> +                    ) : undefined +                  } +                  key={id} +                  meta={ +                    technologies ? ( +                      <MetaList isCentered> +                        <MetaItem +                          hasBorderedValues +                          hasInlinedValues +                          isCentered +                          label={metaLabel} +                          value={technologies.map((techno) => { +                            return { id: techno, value: techno }; +                          })} +                        /> +                      </MetaList> +                    ) : undefined +                  } +                  isCentered +                  linkTo={`${ROUTES.PROJECTS}/${slug}`} +                > +                  <CardHeader> +                    <CardTitle>{projectTitle}</CardTitle> +                  </CardHeader> +                  <CardBody>{tagline}</CardBody> +                  <CardFooter /> +                </Card> +              ); +            } +          )} +        </Grid>        </PageBody>      </Page>    ); | 
