Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions components/BasicCard/BasicCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const BasicCard = ({
body,
ctaLink,
filename,
alt,
focus,
aspectRatio = '1x1',
visibleHorizontal,
Expand Down Expand Up @@ -128,7 +127,6 @@ export const BasicCard = ({
{...props}
bgColor={a11yBgColor}
filename={filename}
alt={alt}
focus={focus}
visibleHorizontal={visibleHorizontal}
visibleVertical={visibleVertical}
Expand Down
1 change: 1 addition & 0 deletions components/CampaignCard/CampaignCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const CampaignCard = ({
filename={filename}
alt={alt}
focus={focus}
imageSize="card"
visibleHorizontal={visibleHorizontal}
visibleVertical={visibleVertical}
className={styles.image}
Expand Down
1 change: 1 addition & 0 deletions components/EmbedVideo/EmbedVideo.styles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const videoAspectRatios = {
'16x9': 'aspect-[16/9]',
'9x16': 'aspect-[9/16]',
'4x3': 'aspect-[4/3]',
'1x1': 'aspect-1',
};
Expand Down
27 changes: 14 additions & 13 deletions components/EmbedVideo/EmbedVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,22 @@ export const EmbedVideo = ({
captionAlign={captionAlign}
pt={pt}
pb={pb}
childrenWrapperClass={styles.videoAspectRatios[aspectRatio]}
{...props}
>
{isClient && (
<ReactPlayer
url={videoUrl}
width="100%"
height="100%"
controls
playsinline
config={{
youtube: { playerVars: { start: startTimeInSeconds } },
}}
/>
)}
<div className={styles.videoAspectRatios[aspectRatio]}>
{isClient && (
<ReactPlayer
url={videoUrl}
width="100%"
height="100%"
controls
playsinline
config={{
youtube: { playerVars: { start: startTimeInSeconds } },
}}
/>
)}
</div>
</MediaWrapper>
);
};
6 changes: 5 additions & 1 deletion components/GallerySlideshow/Slide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export const Slide = ({
<figure {...props}>
<div className={styles.imageWrapper}>
<picture>
<source
srcSet={getProcessedImage(imageSrc, '0x900')}
media="(min-width: 1500px)"
/>
<source
srcSet={getProcessedImage(imageSrc, '0x800')}
media="(min-width: 1200px)"
Expand All @@ -50,7 +54,7 @@ export const Slide = ({
media="(max-width: 767px)"
/>
<img
src={getProcessedImage(imageSrc, '0x800')}
src={getProcessedImage(imageSrc, '0x900')}
alt={alt || ''}
className={styles.image}
/>
Expand Down
82 changes: 56 additions & 26 deletions components/Image/AspectRatioImage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { getAspectRatioNumber } from '@/utilities/getAspectRatioNumber';
import { getProcessedImage } from '@/utilities/getProcessedImage';
import { getSbImageSize } from '@/utilities/getSbImageSize';
import { getAspectRatioNumber } from '@/utilities/getAspectRatioNumber';
import { visiblePositionToFocus } from '@/utilities/visiblePositionToFocus';
import { type SbImageType } from '@/components/Storyblok/Storyblok.types';
import * as styles from './Image.styles';
Expand All @@ -12,17 +12,19 @@ export type AspectRatioImageProps = SbImageType & React.HTMLAttributes<HTMLImage
imageSize?: styles.AspectRatioImageSizeType;
aspectRatio?: styles.ImageAspectRatioType;
fetchPriority?: 'low' | 'high' | 'auto';
loading?: 'eager' | 'lazy';
};

export const AspectRatioImage = ({
filename,
alt,
focus,
imageSize = 'default',
imageSize,
aspectRatio = '3x2',
visibleHorizontal,
visibleVertical,
fetchPriority,
loading = fetchPriority === 'high' ? 'eager' : 'lazy',
className,
...imageProps
}: AspectRatioImageProps) => {
Expand All @@ -36,35 +38,63 @@ export const AspectRatioImage = ({
return visiblePositionToFocus(originalWidth, originalHeight, visibleHorizontal, visibleVertical);
}, [focus, originalWidth, originalHeight, visibleHorizontal, visibleVertical]);

const { cropHeight, cropWidth } = useMemo(() => {
const targetCropWidth = styles.aspectImageSizes[imageSize];

// E.g. '3x2' => 1.5
const aspectRatioDecimal = getAspectRatioNumber(aspectRatio);

const cropWidth = originalWidth > targetCropWidth ? targetCropWidth : originalWidth;
const cropHeight = Math.round(cropWidth / aspectRatioDecimal);

return { cropWidth, cropHeight };
}, [aspectRatio, imageSize, originalWidth]);

if (!filename) {
return null;
}

const processedImg = getProcessedImage(filename, `${cropWidth}x${cropHeight}`, imageFocus);
const aspectRatioNumber = getAspectRatioNumber(aspectRatio);
const desktopCropSize = styles.imageCropsDesktop[aspectRatio];
const desktopCropWidth = parseInt(desktopCropSize.split('x')[0], 10);

const largestWidth = imageSize ? styles.aspectImageSizes[imageSize] : desktopCropWidth;
const largestHeight = Math.round(largestWidth / aspectRatioNumber);
const largestCropString = `${largestWidth}x${largestHeight}`;

return (
<div className={className}>
<img
className={styles.imageAspectRatios[aspectRatio]}
width={cropWidth}
height={cropHeight}
src={processedImg}
fetchPriority={fetchPriority}
alt={alt || ''}
{...imageProps}
/>
</div>
<>
{!imageSize ? (
<picture>
{originalWidth >= desktopCropWidth && (
<source
srcSet={getProcessedImage(filename, largestCropString, imageFocus)}
media="(min-width: 1500px)"
/>
)}
<source
srcSet={getProcessedImage(filename, styles.imageCropsSmallDesktop[aspectRatio], imageFocus)}
media="(min-width: 992px)"
/>
<source
srcSet={getProcessedImage(filename, styles.imageCropsTablet[aspectRatio], imageFocus)}
media="(min-width: 576px)"
/>
<source
srcSet={getProcessedImage(filename, styles.imageCropsMobile[aspectRatio], imageFocus)}
media="(max-width: 575px)"
/>
<img
{...imageProps}
width={largestWidth}
height={largestHeight}
src={getProcessedImage(filename, largestCropString, imageFocus)}
fetchPriority={fetchPriority}
loading={loading}
alt={alt || ''}
className={className}
/>
</picture>
) : (
<img
{...imageProps}
width={largestWidth}
height={largestHeight}
src={getProcessedImage(filename, largestCropString, imageFocus)}
fetchPriority={fetchPriority}
loading={loading}
alt={alt || ''}
className={className}
/>
)}
</>
);
};
16 changes: 9 additions & 7 deletions components/Image/FullWidthImage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useMemo } from 'react';
import { cnb } from 'cnbuilder';
import { getSbImageSize } from '@/utilities/getSbImageSize';
import { getImageSources } from '@/utilities/getImageSources';
import { getSbImageSize } from '@/utilities/getSbImageSize';
import { type SbImageType } from '@/components/Storyblok/Storyblok.types';
import * as styles from './Image.styles';

export type FullWidthImageProps = SbImageType & React.HTMLAttributes<HTMLImageElement> & {
visibleVertical?: styles.VisibleVerticalType;
visibleHorizontal?: styles.VisibleHorizontalType;
fetchPriority?: 'low' | 'high' | 'auto';
loading?: 'eager' | 'lazy';
};

export const FullWidthImage = ({
Expand All @@ -17,31 +18,32 @@ export const FullWidthImage = ({
visibleHorizontal,
visibleVertical,
fetchPriority,
loading = fetchPriority === 'high' ? 'eager' : 'lazy',
className,
}: FullWidthImageProps) => {
const { width: originalWidth, height: originalHeight } = getSbImageSize(filename);

// Get corresponding image sources for responsive images
const imageSources = useMemo(() => {
return getImageSources(filename, originalWidth);
}, [originalWidth, filename]);
return getImageSources(filename);
}, [filename]);

return (
<div className={className}>
<picture>
{imageSources.map(({ srcSet, media }, index) => (
{imageSources.map(({ srcSet, media }) => (
<source
key={`source-${index}`}
key={srcSet}
srcSet={srcSet}
media={media}
/>
))}
<img
src={imageSources[0].srcSet} // Use the first source as the default image
src={imageSources[0]?.srcSet} // Use the largest source as the default image
alt={alt || ''}
width={originalWidth}
height={originalHeight}
fetchPriority={fetchPriority}
loading={loading}
className={cnb(
'size-full object-cover',
styles.objectPositions(visibleHorizontal, visibleVertical),
Expand Down
52 changes: 38 additions & 14 deletions components/Image/Image.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@ export const objectPositions = (
*/

export const aspectImageSizes = {
default: 1000,
card: 600,
header: 800,
'horizontal-card': 800,
'gallery-slide': 1400,
'large-card': 800,
thumbnail: 400,
};
Expand All @@ -55,45 +53,71 @@ export const imageFocusVertical = (imgHeight: number) => ({
/**
* StoryImage styles
*/

export const imageAspectRatios = {
'1x1': 'aspect-1',
'3x2': 'aspect-[3/2]',
'16x9': 'aspect-[16/9]',
};
export type ImageAspectRatioType = keyof typeof imageAspectRatios;
export type ImageAspectRatioType = '1x1' | '1x2' | '2x1' | '2x3' | '3x2' | '3x4' | '4x3' | '5x8' | '8x5' | '9x16' | '10x3' | '16x9';

// 2XL and up >= 1500px
export const imageCropsDesktop = {
'1x1': '2000x2000',
'1x1': '1400x1400', // We rarely have square or portrait images edge to edge so they can be smaller than the viewport size
'1x2': '1000x2000',
'2x1': '2000x1000',
'2x3': '1200x1800',
'3x2': '2100x1400',
'3x4': '1500x2000',
'4x3': '2000x1500',
'5x8': '1000x1600',
'8x5': '2000x1250',
'9x16': '900x1600',
'10x3': '2000x600',
'16x9': '2000x1125',
'free': '2000x0',
};
export type ImageCropType = keyof typeof imageCropsDesktop;

// LG-XL - 992px - 1499px
export const imageCropsSmallDesktop = {
'1x1': '1000x1000',
'1x2': '1000x2000',
'2x1': '1500x750',
'2x3': '1200x1800',
'3x2': '1500x1000',
'3x4': '1200x1600',
'4x3': '1600x1200',
'5x8': '1000x1600',
'8x5': '1600x1000',
'9x16': '900x1600',
'10x3': '1500x450',
'16x9': '1600x900',
'free': '1500x0',
};

// SM-MD - 576px - 991px
export const imageCropsTablet = {
'1x1': '1000x1000',
'1x2': '1000x2000',
'2x1': '1000x500',
'2x3': '1000x1500',
'3x2': '1000x667',
'3x4': '1000x1333',
'4x3': '1000x750',
'5x8': '1000x1600',
'8x5': '1000x625',
'9x16': '900x1600',
'10x3': '1000x300',
'16x9': '1000x563',
'free': '1000x0',
};

// XS - up to 575px
export const imageCropsMobile = {
'1x1': '600x600',
'1x2': '600x1200',
'2x1': '600x300',
'2x3': '600x900',
'3x2': '600x400',
'3x4': '600x800',
'4x3': '600x450',
'5x8': '600x960',
'8x5': '640x400',
'9x16': '630x1120',
'10x3': '600x180',
'16x9': '640x360',
'free': '600x0',
};

export const image = 'size-full object-cover';
Loading