diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-09 18:26:23 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 15522ec9146f6f1956620355c44dea2a6a75b67c (patch) | |
| tree | 7be0c4ca96cb3e59d2ee989785a6b6a286e6169d | |
| parent | 891441a76173c708c6604fa203b175aefa222333 (diff) | |
refactor(components): replace ResponsiveImage with Figure component
The styles applied to ResponsiveImage are related to the figure and
figcaption elements. Those elements could be use with other contents
than images. So I extracted them in a Figure component. The
ResponsiveImage component is no longer useful: the consumer should use
the Image component from `next` and wrap it in a link if needed.
31 files changed, 337 insertions, 502 deletions
diff --git a/src/components/atoms/figure/figure.module.scss b/src/components/atoms/figure/figure.module.scss new file mode 100644 index 0000000..e7ba5c2 --- /dev/null +++ b/src/components/atoms/figure/figure.module.scss @@ -0,0 +1,30 @@ +@use "../../../styles/abstracts/functions" as fun; + +.caption { + margin: 0; + padding: fun.convert-px(4) var(--spacing-2xs); + background: var(--color-bg-secondary); + border: fun.convert-px(1) solid var(--color-border-light); + font-size: var(--font-size-sm); + font-weight: 500; +} + +.wrapper { + display: flex; + flex-flow: column; + width: fit-content; + margin: 0 auto; + position: relative; + text-align: center; + + &--has-borders { + padding: fun.convert-px(4); + border: fun.convert-px(1) solid var(--color-border); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 + var(--color-shadow); + + .caption { + margin-top: fun.convert-px(4); + } + } +} diff --git a/src/components/atoms/figure/figure.stories.tsx b/src/components/atoms/figure/figure.stories.tsx new file mode 100644 index 0000000..7763641 --- /dev/null +++ b/src/components/atoms/figure/figure.stories.tsx @@ -0,0 +1,74 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import NextImage from 'next/image'; +import { Figure } from './figure'; + +/** + * Figure - Storybook Meta + */ +export default { + title: 'Atoms/Figure', + component: Figure, + args: {}, + argTypes: { + caption: { + control: { + type: 'text', + }, + description: 'A figure caption.', + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + hasBorders: { + control: { + type: 'boolean', + }, + description: 'Add borders around the figure.', + table: { + category: 'Styles', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, +} as ComponentMeta<typeof Figure>; + +const Template: ComponentStory<typeof Figure> = (args) => <Figure {...args} />; + +/** + * Figure Stories - Illustration + */ +export const Illustration = Template.bind({}); +Illustration.args = { + children: ( + <NextImage + alt="An example" + height={480} + src="https://picsum.photos/640/480" + width={640} + /> + ), +}; + +/** + * Figure Stories - BorderedIllustration + */ +export const BorderedIllustration = Template.bind({}); +BorderedIllustration.args = { + children: ( + <NextImage + alt="An example" + height={480} + src="https://picsum.photos/640/480" + width={640} + /> + ), + hasBorders: true, +}; diff --git a/src/components/atoms/figure/figure.test.tsx b/src/components/atoms/figure/figure.test.tsx new file mode 100644 index 0000000..90a07c7 --- /dev/null +++ b/src/components/atoms/figure/figure.test.tsx @@ -0,0 +1,32 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Figure } from './figure'; + +describe('Figure', () => { + it('renders the figure contents', () => { + const body = 'tempora et quis'; + + render(<Figure>{body}</Figure>); + + expect(rtlScreen.getByRole('figure')).toHaveTextContent(body); + }); + + it('can render its contents with a caption', () => { + const body = 'tempora et quis'; + const caption = 'velit dolores magnam'; + + render(<Figure caption={caption}>{body}</Figure>); + + expect(rtlScreen.getByRole('figure', { name: caption })).toHaveTextContent( + body + ); + }); + + it('can style the figure with borders', () => { + const body = 'tempora et quis'; + + render(<Figure hasBorders>{body}</Figure>); + + expect(rtlScreen.getByRole('figure')).toHaveClass('wrapper--has-borders'); + }); +}); diff --git a/src/components/atoms/figure/figure.tsx b/src/components/atoms/figure/figure.tsx new file mode 100644 index 0000000..4dd5b10 --- /dev/null +++ b/src/components/atoms/figure/figure.tsx @@ -0,0 +1,72 @@ +import { + forwardRef, + type ReactNode, + type ForwardRefRenderFunction, + type HTMLAttributes, + useId, +} from 'react'; +import styles from './figure.module.scss'; + +export type FigureProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & { + /** + * The contents (ie. an image, illustration, diagram, code snippet, etc.). + */ + children: ReactNode; + /** + * A figure caption. + */ + caption?: ReactNode; + /** + * Should we wrap the contents with borders? + */ + hasBorders?: boolean; +}; + +const FigureWithRef: ForwardRefRenderFunction<HTMLElement, FigureProps> = ( + { + 'aria-labelledby': ariaLabelledBy, + caption, + children, + className = '', + hasBorders, + ...props + }, + ref +) => { + const captionId = useId(); + const bordersModifier = hasBorders ? styles['wrapper--has-borders'] : ''; + const figureClass = `${styles.wrapper} ${bordersModifier} ${className}`; + + /** + * We need to ensure that the figcaption is used as an accessible name for the + * figure. In Testing Library, it is not automatically associated, it could + * also be the case in some browsers. However if the consumer provide its own + * `aria-labelled-by` attribute, it should be used instead of the caption (we + * could combine them but we cannot know which order is the more logical). + */ + const figureLabelledBy = + caption && !ariaLabelledBy ? captionId : ariaLabelledBy; + + return ( + <figure + {...props} + aria-labelledby={figureLabelledBy} + className={figureClass} + ref={ref} + > + {children} + {caption ? ( + <figcaption className={styles.caption} id={captionId}> + {caption} + </figcaption> + ) : null} + </figure> + ); +}; + +/** + * Figure component + * + * Render a responsive image wrapped in a figure element. + */ +export const Figure = forwardRef(FigureWithRef); diff --git a/src/components/atoms/figure/index.ts b/src/components/atoms/figure/index.ts new file mode 100644 index 0000000..0f6ad20 --- /dev/null +++ b/src/components/atoms/figure/index.ts @@ -0,0 +1 @@ +export * from './figure'; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index e9c41ed..31beda9 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -1,4 +1,5 @@ export * from './buttons'; +export * from './figure'; export * from './flip'; export * from './forms'; export * from './heading'; diff --git a/src/components/molecules/images/index.ts b/src/components/molecules/images/index.ts index 33ec886..318a6af 100644 --- a/src/components/molecules/images/index.ts +++ b/src/components/molecules/images/index.ts @@ -1,2 +1 @@ export * from './flipping-logo'; -export * from './responsive-image'; diff --git a/src/components/molecules/images/responsive-image.module.scss b/src/components/molecules/images/responsive-image.module.scss deleted file mode 100644 index e4ed4aa..0000000 --- a/src/components/molecules/images/responsive-image.module.scss +++ /dev/null @@ -1,84 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; - -.caption { - margin: 0; - padding: fun.convert-px(4) var(--spacing-2xs); - background: var(--color-bg-secondary); - border: fun.convert-px(1) solid var(--color-border-light); - font-size: var(--font-size-sm); - font-weight: 500; -} - -.wrapper { - display: flex; - flex-flow: column; - width: fit-content; - margin: 0 auto; - position: relative; - text-align: center; - - &--has-borders { - .caption { - margin-top: fun.convert-px(4); - } - } - - &--has-borders#{&}--has-link { - .link { - padding: fun.convert-px(4); - } - } - - &--has-borders#{&}--no-link { - padding: fun.convert-px(4); - border: fun.convert-px(1) solid var(--color-border); - box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 - var(--color-shadow); - } -} - -.img { - max-height: 100%; - object-fit: cover; -} - -.link { - display: flex; - flex-flow: column; - background: none; - border: fun.convert-px(1) solid var(--color-border); - box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 - var(--color-shadow); - text-decoration: none; - - .caption { - color: var(--color-primary-darker); - } - - &:hover, - &:focus { - box-shadow: 0 0 fun.convert-px(2) 0 var(--color-shadow-light), - fun.convert-px(2) fun.convert-px(2) fun.convert-px(4) fun.convert-px(1) - var(--color-shadow-light), - fun.convert-px(4) fun.convert-px(4) fun.convert-px(8) fun.convert-px(2) - var(--color-shadow-light); - transform: scale(var(--scale-up, 1.05)); - } - - &:focus { - .caption { - text-decoration: underline solid var(--color-primary-darker) - fun.convert-px(3); - } - } - - &:active { - box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) - fun.convert-px(1) var(--color-shadow-light); - transform: scale(var(--scale-down, 0.95)); - - .caption { - text-decoration: none; - } - } -} diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx deleted file mode 100644 index cc6b088..0000000 --- a/src/components/molecules/images/responsive-image.stories.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ResponsiveImage } from './responsive-image'; - -/** - * ResponsiveImage - Storybook Meta - */ -export default { - title: 'Molecules/Images/ResponsiveImage', - component: ResponsiveImage, - args: { - withBorders: false, - }, - argTypes: { - alt: { - control: { - type: 'text', - }, - description: 'An alternative text.', - type: { - name: 'string', - required: true, - }, - }, - caption: { - control: { - type: 'text', - }, - description: 'A figure caption.', - table: { - category: 'Options', - }, - type: { - name: 'string', - required: false, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the image wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - height: { - control: { - type: 'number', - }, - description: 'The image height.', - type: { - name: 'string', - required: true, - }, - }, - src: { - control: { - type: 'text', - }, - description: 'The image source.', - type: { - name: 'string', - required: true, - }, - }, - target: { - control: { - type: 'text', - }, - description: 'A link target.', - table: { - category: 'Options', - }, - type: { - name: 'string', - required: false, - }, - }, - width: { - control: { - type: 'number', - }, - description: 'The image width.', - type: { - name: 'string', - required: true, - }, - }, - withBorders: { - control: { - type: 'boolean', - }, - description: 'Add borders around the image.', - table: { - category: 'Styles', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta<typeof ResponsiveImage>; - -const Template: ComponentStory<typeof ResponsiveImage> = (args) => ( - <ResponsiveImage {...args} /> -); - -/** - * Responsive Image Stories - Default - */ -export const Default = Template.bind({}); -Default.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, -}; - -/** - * Responsive Image Stories - With borders - */ -export const WithBorders = Template.bind({}); -WithBorders.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - withBorders: true, -}; - -/** - * Responsive Image Stories - With link - */ -export const WithLink = Template.bind({}); -WithLink.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - target: '#', -}; - -/** - * Responsive Image Stories - With link and borders - */ -export const WithLinkAndBorders = Template.bind({}); -WithLinkAndBorders.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - target: '#', - withBorders: true, -}; - -/** - * Responsive Image Stories - With caption - */ -export const WithCaption = Template.bind({}); -WithCaption.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - caption: 'Omnis nulla labore', -}; - -/** - * Responsive Image Stories - With caption and borders - */ -export const WithCaptionAndBorders = Template.bind({}); -WithCaptionAndBorders.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - caption: 'Omnis nulla labore', - withBorders: true, -}; - -/** - * Responsive Image Stories - With caption and link - */ -export const WithCaptionAndLink = Template.bind({}); -WithCaptionAndLink.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - caption: 'Omnis nulla labore', - target: '#', -}; - -/** - * Responsive Image Stories - With caption, link and borders - */ -export const WithCaptionLinkAndBorders = Template.bind({}); -WithCaptionLinkAndBorders.args = { - alt: 'An example', - src: 'http://placeimg.com/640/480/transport', - width: 640, - height: 480, - caption: 'Omnis nulla labore', - target: '#', - withBorders: true, -}; diff --git a/src/components/molecules/images/responsive-image.test.tsx b/src/components/molecules/images/responsive-image.test.tsx deleted file mode 100644 index dec36ea..0000000 --- a/src/components/molecules/images/responsive-image.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { ResponsiveImage } from './responsive-image'; - -describe('ResponsiveImage', () => { - it('renders a responsive image', () => { - render( - <ResponsiveImage - src="http://placeimg.com/640/480" - alt="An alternative text" - width={640} - height={480} - /> - ); - expect( - screen.getByRole('img', { name: 'An alternative text' }) - ).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx deleted file mode 100644 index 85c0c46..0000000 --- a/src/components/molecules/images/responsive-image.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import Image, { type ImageProps } from 'next/image'; -import { FC, ReactNode } from 'react'; -import { Link, type LinkProps } from '../../atoms'; -import styles from './responsive-image.module.scss'; - -export type ResponsiveImageProps = Omit< - ImageProps, - 'alt' | 'width' | 'height' -> & { - /** - * An alternative text. - */ - alt: string; - /** - * A figure caption. - */ - caption?: ReactNode; - /** - * Set additional classnames to the figure wrapper. - */ - className?: string; - /** - * The image height. - */ - height: number | `${number}`; - /** - * A link target. - */ - target?: LinkProps['href']; - /** - * The image width. - */ - width: number | `${number}`; - /** - * Wrap the image with borders. - */ - withBorders?: boolean; -}; - -/** - * ResponsiveImage component - * - * Render a responsive image wrapped in a figure element. - */ -export const ResponsiveImage: FC<ResponsiveImageProps> = ({ - alt, - caption, - className = '', - target, - title, - withBorders, - ...props -}) => { - const bordersModifier = withBorders ? styles['wrapper--has-borders'] : ''; - const linkModifier = target - ? styles['wrapper--has-link'] - : styles['wrapper--no-link']; - const figureClass = `${styles.wrapper} ${bordersModifier} ${linkModifier} ${className}`; - - return ( - <figure aria-label={caption ? undefined : title} className={figureClass}> - {target ? ( - <Link href={target} className={styles.link}> - <Image - {...props} - alt={alt} - className={styles.img} - sizes="100vw" - title={title} - /> - {caption && ( - <figcaption className={styles.caption}>{caption}</figcaption> - )} - </Link> - ) : ( - <> - <Image - {...props} - alt={alt} - className={styles.img} - sizes="100vw" - title={title} - /> - {caption && ( - <figcaption className={styles.caption}>{caption}</figcaption> - )} - </> - )} - </figure> - ); -}; diff --git a/src/components/molecules/layout/card.fixture.tsx b/src/components/molecules/layout/card.fixture.ts index f96cc43..01fe2e9 100644 --- a/src/components/molecules/layout/card.fixture.tsx +++ b/src/components/molecules/layout/card.fixture.ts @@ -1,7 +1,7 @@ export const cover = { alt: 'A picture', height: 480, - src: 'http://placeimg.com/640/480', + src: 'https://picsum.photos/640/480', width: 640, }; diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss index 31f6a4b..7a06508 100644 --- a/src/components/molecules/layout/card.module.scss +++ b/src/components/molecules/layout/card.module.scss @@ -20,6 +20,7 @@ .cover { place-content: center; height: fun.convert-px(150); + object-fit: scale-down; margin: auto; border-bottom: fun.convert-px(1) solid var(--color-border); } diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx index c9e7a90..c316100 100644 --- a/src/components/molecules/layout/card.tsx +++ b/src/components/molecules/layout/card.tsx @@ -1,7 +1,6 @@ +import NextImage, { type ImageProps as NextImageProps } from 'next/image'; import type { FC } from 'react'; -import type { Image as Img } from '../../../types'; -import { ButtonLink, Heading, type HeadingLevel } from '../../atoms'; -import { ResponsiveImage } from '../images'; +import { ButtonLink, Figure, Heading, type HeadingLevel } from '../../atoms'; import styles from './card.module.scss'; import { Meta, type MetaData } from './meta'; @@ -13,7 +12,7 @@ export type CardProps = { /** * The card cover. */ - cover?: Img; + cover?: Pick<NextImageProps, 'alt' | 'src' | 'title' | 'width' | 'height'>; /** * The card id. */ @@ -63,7 +62,9 @@ export const Card: FC<CardProps> = ({ <article className={styles.article}> <header className={styles.header}> {cover ? ( - <ResponsiveImage {...cover} className={styles.cover} /> + <Figure> + <NextImage {...cover} className={styles.cover} /> + </Figure> ) : null} <Heading className={styles.title} id={headingId} level={titleLevel}> {title} diff --git a/src/components/organisms/images/gallery.stories.tsx b/src/components/organisms/images/gallery.stories.tsx index 5005ed8..016b18e 100644 --- a/src/components/organisms/images/gallery.stories.tsx +++ b/src/components/organisms/images/gallery.stories.tsx @@ -1,5 +1,6 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ResponsiveImage } from '../../molecules'; +import NextImage from 'next/image'; +import { Figure } from '../../atoms'; import { Gallery } from './gallery'; /** @@ -13,7 +14,7 @@ export default { control: { type: null, }, - description: 'Two or more ResponsiveImage component.', + description: 'Two or more images.', type: { name: 'function', required: true, @@ -37,16 +38,24 @@ export default { const image = { alt: 'Modi provident omnis', height: 480, - src: 'http://picsum.photos/640/480', + src: 'https://picsum.photos/640/480', width: 640, }; const Template: ComponentStory<typeof Gallery> = (args) => ( <Gallery {...args}> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> + <Figure> + <NextImage {...image} /> + </Figure> + <Figure> + <NextImage {...image} /> + </Figure> + <Figure> + <NextImage {...image} /> + </Figure> + <Figure> + <NextImage {...image} /> + </Figure> </Gallery> ); diff --git a/src/components/organisms/images/gallery.test.tsx b/src/components/organisms/images/gallery.test.tsx index ea39348..bffc3b2 100644 --- a/src/components/organisms/images/gallery.test.tsx +++ b/src/components/organisms/images/gallery.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { ResponsiveImage } from '../../molecules'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import NextImage from 'next/image'; import { Gallery } from './gallery'; const columns = 3; @@ -8,7 +8,7 @@ const columns = 3; const image = { alt: 'Modi provident omnis', height: 480, - src: 'http://placeimg.com/640/480/fashion', + src: 'http://picsum.photos/640/480', width: 640, }; @@ -16,24 +16,28 @@ describe('Gallery', () => { it('renders the correct number of items', () => { render( <Gallery columns={columns}> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> + <NextImage {...image} /> + <NextImage {...image} /> + <NextImage {...image} /> + <NextImage {...image} /> </Gallery> ); - expect(screen.getAllByRole('listitem')).toHaveLength(4); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(4); }); it('renders the right number of columns', () => { render( <Gallery columns={columns}> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> - <ResponsiveImage {...image} /> + <NextImage {...image} /> + <NextImage {...image} /> + <NextImage {...image} /> + <NextImage {...image} /> </Gallery> ); - expect(screen.getByRole('list')).toHaveClass(`wrapper--${columns}-columns`); + expect(rtlScreen.getByRole('list')).toHaveClass( + `wrapper--${columns}-columns` + ); }); }); diff --git a/src/components/organisms/images/gallery.tsx b/src/components/organisms/images/gallery.tsx index b35acfe..2f17130 100644 --- a/src/components/organisms/images/gallery.tsx +++ b/src/components/organisms/images/gallery.tsx @@ -1,6 +1,5 @@ import { Children, type FC, type ReactElement } from 'react'; import { List, ListItem } from '../../atoms'; -import type { ResponsiveImageProps } from '../../molecules'; import styles from './gallery.module.scss'; // eslint-disable-next-line @typescript-eslint/no-magic-numbers @@ -8,9 +7,9 @@ export type GalleryColumn = 2 | 3 | 4; export type GalleryProps = { /** - * The images using ResponsiveImage component. + * The images. */ - children: ReactElement<ResponsiveImageProps>[]; + children: ReactElement[]; /** * The columns count. */ diff --git a/src/components/organisms/layout/overview.stories.tsx b/src/components/organisms/layout/overview.stories.tsx index be6db72..8f56d3a 100644 --- a/src/components/organisms/layout/overview.stories.tsx +++ b/src/components/organisms/layout/overview.stories.tsx @@ -1,5 +1,5 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Overview, OverviewMeta } from './overview'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Overview, type OverviewMeta } from './overview'; /** * Overview - Storybook Meta @@ -50,7 +50,7 @@ const Template: ComponentStory<typeof Overview> = (args) => ( const cover = { alt: 'picture', height: 480, - src: 'http://placeimg.com/640/480/cats', + src: 'https://picsum.photos/640/480', width: 640, }; diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx index bb319c4..8af58ec 100644 --- a/src/components/organisms/layout/overview.tsx +++ b/src/components/organisms/layout/overview.tsx @@ -1,10 +1,7 @@ +import NextImage, { type ImageProps as NextImageProps } from 'next/image'; import type { FC } from 'react'; -import { - Meta, - type MetaData, - ResponsiveImage, - type ResponsiveImageProps, -} from '../../molecules'; +import { Figure } from '../../atoms'; +import { Meta, type MetaData } from '../../molecules'; import styles from './overview.module.scss'; export type OverviewMeta = Pick< @@ -25,7 +22,7 @@ export type OverviewProps = { /** * The overview cover. */ - cover?: Pick<ResponsiveImageProps, 'alt' | 'src' | 'width' | 'height'>; + cover?: Pick<NextImageProps, 'alt' | 'src' | 'width' | 'height'>; /** * The overview meta. */ @@ -47,7 +44,11 @@ export const Overview: FC<OverviewProps> = ({ return ( <div className={`${styles.wrapper} ${className}`}> - {cover ? <ResponsiveImage className={styles.cover} {...cover} /> : null} + {cover ? ( + <Figure> + <NextImage {...cover} className={styles.cover} /> + </Figure> + ) : null} <Meta className={`${styles.meta} ${metaModifier}`} data={{ ...remainingMeta, technologies }} diff --git a/src/components/organisms/layout/summary.fixture.tsx b/src/components/organisms/layout/summary.fixture.ts index bb3ebcb..6f90b4a 100644 --- a/src/components/organisms/layout/summary.fixture.tsx +++ b/src/components/organisms/layout/summary.fixture.ts @@ -1,9 +1,9 @@ -import { type SummaryMeta } from './summary'; +import type { SummaryMeta } from './summary'; export const cover = { alt: 'A cover', height: 480, - src: 'http://placeimg.com/640/480', + src: 'https://picsum.photos/640/480', width: 640, }; diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss index b6cb4f4..9dc1a69 100644 --- a/src/components/organisms/layout/summary.module.scss +++ b/src/components/organisms/layout/summary.module.scss @@ -46,6 +46,7 @@ height: fun.convert-px(100); max-width: 100%; border: fun.convert-px(1) solid var(--color-border); + object-fit: scale-down; @include mix.media("screen") { @include mix.dimensions("sm") { diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx index e21e4c7..fa3dfe5 100644 --- a/src/components/organisms/layout/summary.tsx +++ b/src/components/organisms/layout/summary.tsx @@ -1,3 +1,4 @@ +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'; @@ -8,19 +9,12 @@ import { type HeadingLevel, Icon, Link, + Figure, } from '../../atoms'; -import { - Meta, - type MetaData, - ResponsiveImage, - type ResponsiveImageProps, -} from '../../molecules'; +import { Meta, type MetaData } from '../../molecules'; import styles from './summary.module.scss'; -export type Cover = Pick< - ResponsiveImageProps, - 'alt' | 'src' | 'width' | 'height' ->; +export type Cover = Pick<NextImageProps, 'alt' | 'src' | 'width' | 'height'>; export type SummaryMeta = Pick< MetaType<'article'>, @@ -108,7 +102,11 @@ export const Summary: FC<SummaryProps> = ({ return ( <article className={styles.wrapper}> - {cover ? <ResponsiveImage className={styles.cover} {...cover} /> : null} + {cover ? ( + <Figure> + <NextImage {...cover} className={styles.cover} /> + </Figure> + ) : null} <header className={styles.header}> <Link href={url} className={styles.link}> <Heading level={titleLevel} className={styles.title}> diff --git a/src/components/organisms/widgets/image-widget.module.scss b/src/components/organisms/widgets/image-widget.module.scss index 2174d5b..25de03e 100644 --- a/src/components/organisms/widgets/image-widget.module.scss +++ b/src/components/organisms/widgets/image-widget.module.scss @@ -4,6 +4,7 @@ --scale-up: 1.02; --scale-down: 0.98; + width: fit-content; margin: 0; padding: fun.convert-px(5); border: fun.convert-px(1) solid var(--color-border); diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx index 07c4b11..5de8dd8 100644 --- a/src/components/organisms/widgets/image-widget.tsx +++ b/src/components/organisms/widgets/image-widget.tsx @@ -1,18 +1,12 @@ +import NextImage, { type ImageProps as NextImageProps } from 'next/image'; import type { FC } from 'react'; -import { - ResponsiveImage, - type ResponsiveImageProps, - Collapsible, - type CollapsibleProps, -} from '../../molecules'; +import { Figure, Link, type FigureProps } from '../../atoms'; +import { Collapsible, type CollapsibleProps } from '../../molecules'; import styles from './image-widget.module.scss'; export type Alignment = 'left' | 'center' | 'right'; -export type Image = Pick< - ResponsiveImageProps, - 'alt' | 'height' | 'src' | 'width' ->; +export type Image = Pick<NextImageProps, 'alt' | 'height' | 'src' | 'width'>; export type ImageWidgetProps = Omit< CollapsibleProps, @@ -25,7 +19,7 @@ export type ImageWidgetProps = Omit< /** * Add a caption to the image. */ - description?: ResponsiveImageProps['caption']; + description?: FigureProps['caption']; /** * An object describing the image. */ @@ -37,7 +31,7 @@ export type ImageWidgetProps = Omit< /** * Add a link to the image. */ - url?: ResponsiveImageProps['target']; + url?: string; }; /** @@ -62,12 +56,19 @@ export const ImageWidget: FC<ImageWidgetProps> = ({ {...props} className={`${styles[alignmentClass]} ${className}`} > - <ResponsiveImage - {...image} + <Figure caption={description} className={`${styles.figure} ${imageClassName}`} - target={url} - /> + hasBorders + > + {url ? ( + <Link href={url}> + <NextImage {...image} /> + </Link> + ) : ( + <NextImage {...image} /> + )} + </Figure> </Collapsible> ); }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 60744b7..9c33d2a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -107,6 +107,10 @@ "defaultMessage": "Read more articles about:", "description": "ArticlePage: footer topics list label" }, + "52H2HA": { + "defaultMessage": "{website} logo", + "description": "Layout: logo title" + }, "5O2vpy": { "defaultMessage": "No results found.", "description": "NoResults: no results" @@ -143,6 +147,10 @@ "defaultMessage": "Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.", "description": "AckeeToggle: tooltip message" }, + "8jjY1X": { + "defaultMessage": "{website} picture", + "description": "Layout: photo alternative text" + }, "92zgdp": { "defaultMessage": "Total:", "description": "Meta: total label" @@ -463,10 +471,6 @@ "defaultMessage": "Sidebar", "description": "PageLayout: accessible name for the sidebar" }, - "dDK5oc": { - "defaultMessage": "{website} picture", - "description": "Branding: photo alternative text" - }, "dz2kDV": { "defaultMessage": "Comment form", "description": "CommentForm: aria label" @@ -663,10 +667,6 @@ "defaultMessage": "Free", "description": "HomePage: link to free thematic" }, - "x55qsD": { - "defaultMessage": "{website} logo", - "description": "Branding: logo title" - }, "xYNeKX": { "defaultMessage": "Settings form", "description": "SettingsModal: an accessible form name" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 6c1ee26..997e0e0 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -107,6 +107,10 @@ "defaultMessage": "Lire plus d’articles à propos de :", "description": "ArticlePage: footer topics list label" }, + "52H2HA": { + "defaultMessage": "Logo du site d’{website}", + "description": "Layout: logo title" + }, "5O2vpy": { "defaultMessage": "Aucun résultat.", "description": "NoResults: no results" @@ -143,6 +147,10 @@ "defaultMessage": "Complet inclut toutes les informations de Partiel ainsi que des informations à propos du site référent, du système d’exploitation, de l’appareil, du navigateur, de la taille d’écran et de la langue.", "description": "AckeeToggle: tooltip message" }, + "8jjY1X": { + "defaultMessage": "Photo d’{website}", + "description": "Layout: photo alternative text" + }, "92zgdp": { "defaultMessage": "Total :", "description": "Meta: total label" @@ -463,10 +471,6 @@ "defaultMessage": "Barre latérale", "description": "PageLayout: accessible name for the sidebar" }, - "dDK5oc": { - "defaultMessage": "Photo d’{website}", - "description": "Branding: photo alternative text" - }, "dz2kDV": { "defaultMessage": "Formulaire des commentaires", "description": "CommentForm: aria label" @@ -663,10 +667,6 @@ "defaultMessage": "Libre", "description": "HomePage: link to free thematic" }, - "x55qsD": { - "defaultMessage": "Logo d’{website}", - "description": "Branding: logo title" - }, "xYNeKX": { "defaultMessage": "Formulaire des réglages", "description": "SettingsModal: an accessible form name" diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index 523e21d..acb80b2 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -2,6 +2,7 @@ import type { ParsedUrlQuery } from 'querystring'; import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; +import NextImage from 'next/image'; import { useRouter } from 'next/router'; import Script from 'next/script'; import type { HTMLAttributes } from 'react'; @@ -12,7 +13,6 @@ import { Link, PageLayout, type PageLayoutProps, - ResponsiveImage, Sharing, Spinner, } from '../../components'; @@ -108,7 +108,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ label: footerMetaLabel, value: topics.map((topic) => ( <ButtonLink className={styles.btn} key={topic.id} to={topic.url}> - {topic.logo ? <ResponsiveImage {...topic.logo} /> : null} {topic.name} + {topic.logo ? <NextImage {...topic.logo} /> : null} {topic.name} </ButtonLink> )), }, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 1f1c9f3..d94160f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,7 @@ 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 Script from 'next/script'; import type { FC, HTMLAttributes } from 'react'; import { useIntl } from 'react-intl'; @@ -15,10 +16,10 @@ import { Icon, List, ListItem, - ResponsiveImage, Section, type SectionProps, Heading, + Figure, } from '../components'; import HomePageContent from '../content/pages/homepage.mdx'; import { getArticlesCard } from '../services/graphql'; @@ -83,6 +84,12 @@ const H6 = ({ </Heading> ); +const ResponsiveImage = (props: NextImageProps) => ( + <Figure> + <NextImage {...props} /> + </Figure> +); + /** * Retrieve a list of coding links. * diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx index 9b0cc98..810d9ec 100644 --- a/src/pages/mentions-legales.tsx +++ b/src/pages/mentions-legales.tsx @@ -1,14 +1,15 @@ 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 { getLayout, Link, PageLayout, - ResponsiveImage, type MetaData, + Figure, } from '../components'; import LegalNoticeContent, { meta } from '../content/pages/legal-notice.mdx'; import type { NextPageWithLayout } from '../types'; @@ -21,6 +22,12 @@ import { import { loadTranslation } from '../utils/helpers/server'; import { useBreadcrumb, useSettings } from '../utils/hooks'; +const ResponsiveImage = (props: NextImageProps) => ( + <Figure> + <NextImage {...props} /> + </Figure> +); + const components: MDXComponents = { Image: ResponsiveImage, Link, diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index ee86c7b..0b94a4e 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -3,6 +3,7 @@ import type { MDXComponents } from 'mdx/types'; import type { GetStaticPaths, GetStaticProps } from 'next'; import dynamic from 'next/dynamic'; 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 type { ComponentType, HTMLAttributes } from 'react'; @@ -15,8 +16,6 @@ import { Overview, type OverviewMeta, PageLayout, - ResponsiveImage, - type ResponsiveImageProps, Sharing, SocialLink, Spinner, @@ -24,6 +23,7 @@ import { Heading, List, ListItem, + Figure, } from '../../components'; import styles from '../../styles/pages/project.module.scss'; import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types'; @@ -41,8 +41,10 @@ import { } from '../../utils/helpers/server'; import { useBreadcrumb, useGithubApi, useSettings } from '../../utils/hooks'; -const BorderedImage = (props: ResponsiveImageProps) => ( - <ResponsiveImage withBorders={true} {...props} /> +const BorderedImage = (props: NextImageProps) => ( + <Figure hasBorders> + <NextImage {...props} /> + </Figure> ); const H1 = ({ diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx index 8e3100f..899f9e1 100644 --- a/src/pages/sujet/[slug].tsx +++ b/src/pages/sujet/[slug].tsx @@ -2,6 +2,7 @@ import type { ParsedUrlQuery } from 'querystring'; import type { GetStaticPaths, GetStaticProps } from 'next'; import Head from 'next/head'; +import NextImage from 'next/image'; import { useRouter } from 'next/router'; import Script from 'next/script'; import { useIntl } from 'react-intl'; @@ -11,7 +12,6 @@ import { LinksListWidget, PageLayout, PostsList, - ResponsiveImage, type MetaData, } from '../../components'; import { @@ -101,7 +101,7 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ const getPageHeading = () => ( <> - {cover ? <ResponsiveImage className={styles.logo} {...cover} /> : null} + {cover ? <NextImage {...cover} className={styles.logo} /> : null} {title} </> ); |
