diff options
Diffstat (limited to 'src/components/molecules/images')
8 files changed, 615 insertions, 0 deletions
diff --git a/src/components/molecules/images/flipping-logo.module.scss b/src/components/molecules/images/flipping-logo.module.scss new file mode 100644 index 0000000..89b9499 --- /dev/null +++ b/src/components/molecules/images/flipping-logo.module.scss @@ -0,0 +1,59 @@ +@use "@styles/abstracts/functions" as fun; + +.logo { + width: var(--logo-size, fun.convert-px(100)); + height: var(--logo-size, fun.convert-px(100)); + position: relative; + border-radius: 50%; + transform-style: preserve-3d; + transition: all 0.6s linear 0s; + + &__front, + &__back { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + backface-visibility: hidden; + background: var(--color-bg); + border: fun.convert-px(2) solid var(--color-primary-dark); + border-radius: 50%; + transition: all 0.6s linear 0s; + + svg, + img { + // !important is required to override next/image styles... + padding: fun.convert-px(2) !important; + border-radius: 50%; + } + } + + &__front { + box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0 + var(--color-shadow-light), + fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0 + var(--color-shadow-light); + } + + &__back { + transform: rotateY(180deg); + } + + &:hover { + transform: rotateY(180deg); + } + + &:hover & { + &__front { + box-shadow: none; + } + + &__back { + box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0 + var(--color-shadow-light), + fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0 + var(--color-shadow-light); + } + } +} diff --git a/src/components/molecules/images/flipping-logo.stories.tsx b/src/components/molecules/images/flipping-logo.stories.tsx new file mode 100644 index 0000000..9d09293 --- /dev/null +++ b/src/components/molecules/images/flipping-logo.stories.tsx @@ -0,0 +1,72 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import FlippingLogoComponent from './flipping-logo'; + +/** + * FlippingLogo - Storybook Meta + */ +export default { + title: 'Molecules/Images', + component: FlippingLogoComponent, + argTypes: { + altText: { + control: { + type: 'text', + }, + description: 'Photo alternative text.', + type: { + name: 'string', + required: true, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the logo wrapper.', + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + logoTitle: { + control: { + type: 'text', + }, + description: 'An accessible name for the logo.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + photo: { + control: { + type: 'text', + }, + description: 'Photo url.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof FlippingLogoComponent>; + +const Template: ComponentStory<typeof FlippingLogoComponent> = (args) => ( + <FlippingLogoComponent {...args} /> +); + +/** + * Images Stories - Flipping Logo + */ +export const FlippingLogo = Template.bind({}); +FlippingLogo.args = { + altText: 'Website picture', + logoTitle: 'Website logo', + photo: 'http://placeimg.com/640/480', +}; diff --git a/src/components/molecules/images/flipping-logo.test.tsx b/src/components/molecules/images/flipping-logo.test.tsx new file mode 100644 index 0000000..806fdbe --- /dev/null +++ b/src/components/molecules/images/flipping-logo.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@test-utils'; +import FlippingLogo from './flipping-logo'; + +describe('FlippingLogo', () => { + it('renders a photo', () => { + render( + <FlippingLogo + altText="Alternative text" + photo="http://placeimg.com/640/480" + /> + ); + expect(screen.getByAltText('Alternative text')).toBeInTheDocument(); + }); + + it('renders a logo', () => { + render( + <FlippingLogo + altText="Alternative text" + logoTitle="A logo title" + photo="http://placeimg.com/640/480" + /> + ); + expect(screen.getByTitle('A logo title')).toBeInTheDocument(); + }); +}); diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx new file mode 100644 index 0000000..1099d53 --- /dev/null +++ b/src/components/molecules/images/flipping-logo.tsx @@ -0,0 +1,55 @@ +import Logo, { type LogoProps } from '@components/atoms/images/logo'; +import Image, { type ImageProps } from 'next/image'; +import { ForwardedRef, forwardRef, ForwardRefRenderFunction } from 'react'; +import styles from './flipping-logo.module.scss'; + +export type FlippingLogoProps = { + /** + * Set additional classnames to the logo wrapper. + */ + className?: string; + /** + * Photo alternative text. + */ + altText: string; + /** + * Logo image title. + */ + logoTitle?: LogoProps['title']; + /** + * Photo url. + */ + photo: ImageProps['src']; +}; + +/** + * FlippingLogo component + * + * Render a logo and a photo with a flipping effect. + */ +const FlippingLogo: ForwardRefRenderFunction< + HTMLDivElement, + FlippingLogoProps +> = ( + { className = '', altText, logoTitle, photo, ...props }, + ref: ForwardedRef<HTMLDivElement> +) => { + return ( + <div className={`${styles.logo} ${className}`} ref={ref}> + <div className={styles.logo__front}> + <Image + src={photo} + alt={altText} + layout="fill" + objectFit="cover" + {...props} + /> + </div> + <div className={styles.logo__back}> + <Logo title={logoTitle} /> + </div> + </div> + ); +}; + +export default forwardRef(FlippingLogo); 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..8a1d51f --- /dev/null +++ b/src/components/molecules/images/responsive-image.module.scss @@ -0,0 +1,79 @@ +@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); + } +} + +.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 new file mode 100644 index 0000000..4917cde --- /dev/null +++ b/src/components/molecules/images/responsive-image.stories.tsx @@ -0,0 +1,212 @@ +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 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..4541df8 --- /dev/null +++ b/src/components/molecules/images/responsive-image.tsx @@ -0,0 +1,95 @@ +import Link, { type LinkProps } from '@components/atoms/links/link'; +import Image, { type ImageProps } from 'next/image'; +import { FC, ReactNode } from 'react'; +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 | string; + /** + * A link target. + */ + target?: LinkProps['href']; + /** + * The image width. + */ + width: number | string; + /** + * Wrap the image with borders. + */ + withBorders?: boolean; +}; + +/** + * ResponsiveImage component + * + * Render a responsive image wrapped in a figure element. + */ +const ResponsiveImage: FC<ResponsiveImageProps> = ({ + alt, + caption, + className = '', + layout, + objectFit, + target, + withBorders, + ...props +}) => { + const bordersModifier = withBorders + ? 'wrapper--has-borders' + : 'wrapper--no-borders'; + const linkModifier = target ? 'wrapper--has-link' : 'wrapper--no-link'; + + return ( + <figure + className={`${styles.wrapper} ${styles[bordersModifier]} ${styles[linkModifier]} ${className}`} + > + {target ? ( + <Link href={target} className={styles.link}> + <Image + alt={alt} + layout={layout || 'intrinsic'} + objectFit={objectFit || 'contain'} + className={styles.img} + {...props} + /> + {caption && ( + <figcaption className={styles.caption}>{caption}</figcaption> + )} + </Link> + ) : ( + <> + <Image + alt={alt} + layout={layout || 'intrinsic'} + objectFit={objectFit || 'contain'} + className={styles.img} + {...props} + /> + {caption && ( + <figcaption className={styles.caption}>{caption}</figcaption> + )} + </> + )} + </figure> + ); +}; + +export default ResponsiveImage; |
