summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/atoms/links/link.tsx18
-rw-r--r--src/components/molecules/images/responsive-image.module.scss52
-rw-r--r--src/components/molecules/images/responsive-image.stories.tsx87
-rw-r--r--src/components/molecules/images/responsive-image.test.tsx18
-rw-r--r--src/components/molecules/images/responsive-image.tsx68
5 files changed, 240 insertions, 3 deletions
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index 0a69c33..a61158f 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -4,6 +4,10 @@ import styles from './link.module.scss';
type LinkProps = {
/**
+ * Set additional classes to the link.
+ */
+ classes?: string;
+ /**
* True if it is an external link. Default: false.
*/
external?: boolean;
@@ -22,18 +26,26 @@ type LinkProps = {
*
* Render a link.
*/
-const Link: FC<LinkProps> = ({ children, href, lang, external = false }) => {
+const Link: FC<LinkProps> = ({
+ children,
+ classes,
+ href,
+ lang,
+ external = false,
+}) => {
+ const additionalClasses = classes || '';
+
return external ? (
<a
href={href}
hrefLang={lang}
- className={`${styles.link} ${styles['link--external']}`}
+ className={`${styles.link} ${styles['link--external']} ${additionalClasses}`}
>
{children}
</a>
) : (
<NextLink href={href}>
- <a className={styles.link}>{children}</a>
+ <a className={`${styles.link} ${additionalClasses}`}>{children}</a>
</NextLink>
);
};
diff --git a/src/components/molecules/images/responsive-image.module.scss b/src/components/molecules/images/responsive-image.module.scss
new file mode 100644
index 0000000..83e8d10
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.module.scss
@@ -0,0 +1,52 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ display: flex;
+ flex-flow: column;
+ width: 100%;
+ max-width: max-content;
+ margin: var(--spacing-sm) auto;
+ position: relative;
+ text-align: center;
+}
+
+.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);
+ box-shadow: 0 fun.convert-px(-1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light);
+ font-weight: 500;
+}
+
+.link {
+ display: flex;
+ flex-flow: column;
+ background: none;
+ text-decoration: none;
+
+ .caption {
+ color: var(--color-primary-darker);
+ }
+
+ &:hover,
+ &:focus {
+ transform: scale(1.1);
+ }
+
+ &:focus {
+ .caption {
+ text-decoration: underline solid var(--color-primary-darker)
+ fun.convert-px(3);
+ }
+ }
+
+ &:active {
+ transform: scale(0.9);
+
+ .caption {
+ text-decoration: none;
+ }
+ }
+}
diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx
new file mode 100644
index 0000000..f9c1d2b
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.stories.tsx
@@ -0,0 +1,87 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ResponsiveImageComponent from './responsive-image';
+
+export default {
+ title: 'Molecules/Images',
+ component: ResponsiveImageComponent,
+ 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,
+ },
+ },
+ 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,
+ },
+ },
+ },
+} as ComponentMeta<typeof ResponsiveImageComponent>;
+
+const Template: ComponentStory<typeof ResponsiveImageComponent> = (args) => (
+ <ResponsiveImageComponent {...args} />
+);
+
+export const ResponsiveImage = Template.bind({});
+ResponsiveImage.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+};
diff --git a/src/components/molecules/images/responsive-image.test.tsx b/src/components/molecules/images/responsive-image.test.tsx
new file mode 100644
index 0000000..5452d28
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.test.tsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@test-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
new file mode 100644
index 0000000..db2f5ab
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -0,0 +1,68 @@
+import Link from '@components/atoms/links/link';
+import Image, { ImageProps } from 'next/image';
+import { FC } from 'react';
+import styles from './responsive-image.module.scss';
+
+type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
+ /**
+ * An alternative text.
+ */
+ alt: string;
+ /**
+ * A figure caption.
+ */
+ caption?: string;
+ /**
+ * The image height.
+ */
+ height: number | string;
+ /**
+ * A link target.
+ */
+ target?: string;
+ /**
+ * The image width.
+ */
+ width: number | string;
+};
+
+/**
+ * ResponsiveImage component
+ *
+ * Render a responsive image wrapped in a figure element.
+ */
+const ResponsiveImage: FC<ResponsiveImageProps> = ({
+ alt,
+ caption,
+ layout,
+ objectFit,
+ target,
+ ...props
+}) => {
+ return (
+ <figure className={styles.wrapper}>
+ {target ? (
+ <Link href={target} classes={styles.link}>
+ <Image alt={alt} layout={layout || 'intrinsic'} {...props} />
+ {caption && (
+ <figcaption className={styles.caption}>{caption}</figcaption>
+ )}
+ </Link>
+ ) : (
+ <>
+ <Image
+ alt={alt}
+ layout={layout || 'intrinsic'}
+ objectFit={objectFit || 'contain'}
+ {...props}
+ />
+ {caption && (
+ <figcaption className={styles.caption}>{caption}</figcaption>
+ )}
+ </>
+ )}
+ </figure>
+ );
+};
+
+export default ResponsiveImage;