Skip to content

Commit 7e6ae6c

Browse files
committed
Keep working on StoryImage and create utility getIimageSources
1 parent e46ac55 commit 7e6ae6c

File tree

9 files changed

+158
-36
lines changed

9 files changed

+158
-36
lines changed

components/Image/FullWidthImage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
22
import { cnb } from 'cnbuilder';
33
import { getProcessedImage } from '@/utilities/getProcessedImage';
44
import { getSbImageSize } from '@/utilities/getSbImageSize';
5+
import { getImageSources } from '@/utilities/getImageSources';
56
import { type SbImageType } from '@/components/Storyblok/Storyblok.types';
67
import * as styles from './Image.styles';
78

@@ -42,7 +43,7 @@ export const FullWidthImage = ({
4243
// For example, if the original image is 1100px, it will be used for the min-width: 992px breakpoint
4344
if (largestBp) {
4445
sources.push({
45-
srcSet: getProcessedImage(filename, ''), // Original size
46+
srcSet: getProcessedImage(filename), // Original size
4647
media: `(min-width: ${largestBp.minWidth}px)`,
4748
width: originalWidth,
4849
height: originalHeight,

components/Image/StoryImage.tsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as styles from './Image.styles';
77

88
export type StoryImageProps = React.HTMLAttributes<HTMLDivElement> & MediaWrapperProps & {
99
imageSrc: string;
10-
imageFocus?: string;
1110
alt?: string;
1211
visibleVertical?: styles.VisibleVerticalType;
1312
backgroundColor?: LightPageBgColorsType;
@@ -16,10 +15,10 @@ export type StoryImageProps = React.HTMLAttributes<HTMLDivElement> & MediaWrappe
1615

1716
export const StoryImage = ({
1817
imageSrc,
19-
imageFocus,
2018
imageWidth,
2119
alt,
2220
caption,
21+
captionAlign,
2322
isCard,
2423
backgroundColor,
2524
visibleVertical,
@@ -29,28 +28,23 @@ export const StoryImage = ({
2928
}: StoryImageProps) => {
3029
const { width: originalWidth, height: originalHeight } = getSbImageSize(imageSrc);
3130
const cropSize = styles.imageCropsDesktop['free'];
32-
/**
33-
* Crop width and height are used for width and height attributes on the img element.
34-
* They don't need to be exact as long as the aspect ratio is correct.
35-
*/
36-
const cropWidth = parseInt(cropSize?.split('x')[0], 10);
37-
// const cropHeight = !aspectRatio || !aspectRatio
38-
// ? Math.round(originalHeight * 1000 / originalWidth)
39-
// : parseInt(cropSize?.split('x')[1], 10);
4031

4132
return (
4233
<MediaWrapper
4334
width={imageWidth !== 'su-w-full' && imageWidth !== 'fit-container' ? 'site' : 'full'}
4435
imageWidth={imageWidth || 'su-w-story'}
4536
caption={caption}
37+
captionAlign={captionAlign}
38+
captionBgColor={backgroundColor}
39+
isCard={isCard}
4640
pt={pt}
4741
pb={pb}
4842
{...props}
4943
>
5044
{!!imageSrc && (
5145
<picture>
5246
<source
53-
srcSet={getProcessedImage(imageSrc, cropSize, imageFocus)}
47+
srcSet={getProcessedImage(imageSrc, cropSize)}
5448
media="(min-width: 1500px)"
5549
/>
5650
{/* <source
@@ -66,9 +60,9 @@ export const StoryImage = ({
6660
media="(max-width: 575px)"
6761
/> */}
6862
<img
69-
src={getProcessedImage(imageSrc, cropSize, imageFocus)}
63+
src={getProcessedImage(imageSrc, cropSize)}
7064
loading="lazy"
71-
width={cropWidth}
65+
// width={cropWidth}
7266
// height={cropHeight}
7367
alt={alt || ''}
7468
className={cnb(styles.image, styles.objectPositions('center', visibleVertical))}

components/Media/Caption.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { cnb } from 'cnbuilder';
2+
import { Container } from '@/components/Container';
3+
import { type StoryImageWidthType } from '@/components/Image';
4+
import { lightPageBgColors, type LightPageBgColorsType } from '@/utilities/datasource';
5+
import { type TextAlignType } from '@/components/Typography';
16
import * as styles from './MediaWrapper.styles';
27

38
/**
@@ -6,17 +11,37 @@ import * as styles from './MediaWrapper.styles';
611
*/
712

813
export type CaptionProps = React.HTMLAttributes<HTMLDivElement> & {
14+
imageWidth?: StoryImageWidthType;
915
caption?: React.ReactNode;
16+
captionAlign?: TextAlignType;
17+
isCard?: boolean;
18+
// Inset the caption to centered container width when the media is edge-to-edge
19+
isCaptionInset?: boolean;
20+
captionBgColor?: LightPageBgColorsType;
1021
};
1122

1223
export const Caption = ({
24+
imageWidth,
1325
caption,
26+
captionAlign,
27+
isCaptionInset,
28+
isCard,
29+
captionBgColor = 'white',
1430
className,
1531
...props
1632
}: CaptionProps) => {
33+
if (!caption) return null;
34+
1735
return (
18-
<figcaption {...props} className={styles.caption} data-component="Caption">
19-
{caption}
20-
</figcaption>
36+
<Container
37+
as="figcaption"
38+
width={isCaptionInset ? 'site' : 'full'}
39+
className={cnb(styles.captionWrapper, lightPageBgColors[captionBgColor])}
40+
{...props}
41+
>
42+
<div className={styles.caption(isCard, imageWidth, captionBgColor, captionAlign)}>
43+
{caption}
44+
</div>
45+
</Container>
2146
);
2247
};
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import { cnb } from 'cnbuilder';
22
import { type StoryImageWidthType } from '@/components/Image';
3+
import { type TextAlignType } from '@/components/Typography';
4+
import { lightPageBgColors, type LightPageBgColorsType } from '@/utilities/datasource';
35

46
export const root = 'relative flex';
57
export const wrapper = (imageWidth: StoryImageWidthType) => cnb('mx-auto', imageWidth === 'su-w-full' && 'w-full');
68

79
// Caption component styles
8-
export const captionWrapper = 'mt-0';
9-
export const caption = '*:*:leading-display *:*:xl:leading-snug max-w-prose-wide first:*:*:mt-0';
10+
export const captionWrapper = 'mt-0 caption';
11+
export const caption = (
12+
isCard: boolean,
13+
imageWidth: StoryImageWidthType,
14+
captionBgColor: LightPageBgColorsType,
15+
captionAlign: TextAlignType,
16+
) => cnb(
17+
'*:*:leading-display *:*:xl:leading-snug first:*:*:mt-0',
18+
isCard ? 'rs-px-0 pt-08em rs-pb-2' : 'pt-06em',
19+
lightPageBgColors[captionBgColor],
20+
{
21+
'text-center mx-auto': captionAlign === 'center' || imageWidth === 'su-w-full',
22+
'text-left': captionAlign === 'left' && imageWidth !== 'su-w-full',
23+
'text-right mr-0': captionAlign === 'right' && imageWidth !== 'su-w-full',
24+
},
25+
);

components/Media/MediaWrapper.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,20 @@
11
import { cnb } from 'cnbuilder';
22
import { Caption, type CaptionProps } from './Caption';
33
import { Container, type ContainerProps } from '@/components/Container';
4-
import { storyImageWidths, type StoryImageWidthType } from '@/components/Image';
5-
import { type TextAlignType } from '@/components/Typography';
4+
import { storyImageWidths } from '@/components/Image';
65
import * as styles from './MediaWrapper.styles';
76

87
/**
98
* This is a wrapper component for images and media elements.
109
* that provides a shared set of layout and caption options.
1110
*/
12-
export type MediaWrapperProps = React.HTMLAttributes<HTMLDivElement> & CaptionProps & ContainerProps & {
13-
captionAlign?: TextAlignType;
14-
imageWidth?: StoryImageWidthType;
15-
// mt?: LargeMarginType;
16-
// mb?: LargeMarginType;
17-
// pt?: PaddingType;
18-
// pb?: PaddingType;
19-
};
11+
export type MediaWrapperProps = React.HTMLAttributes<HTMLDivElement> & CaptionProps & ContainerProps;
2012

2113
export const MediaWrapper = ({
2214
caption,
2315
captionAlign,
16+
isCard,
17+
captionBgColor,
2418
width, // This is the bounding width of the Container
2519
imageWidth, // This is the width of the wrapper inside the Container
2620
mt,
@@ -47,7 +41,14 @@ export const MediaWrapper = ({
4741
{children}
4842
</div>
4943
{caption && (
50-
<Caption caption={caption} />
44+
<Caption
45+
imageWidth={imageWidth}
46+
caption={caption}
47+
isCard={isCard}
48+
captionBgColor={captionBgColor}
49+
captionAlign={captionAlign}
50+
isCaptionInset={imageWidth === 'su-w-full'}
51+
/>
5152
)}
5253
</div>
5354
</Container>

components/RichText/RichText.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ export const RichText = ({
167167
textAligns[textAlign],
168168
className,
169169
)}
170-
data-component="RichText"
171170
>
172171
{rendered}
173172
</div>

components/Storyblok/SbStoryImage.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { type SbImageType } from '@/components/Storyblok/Storyblok.types';
99

1010
type SbStoryImageProps = {
1111
blok: SbBlokData & {
12-
_uid: string;
1312
image: SbImageType;
1413
alt?: string;
1514
imageWidth?: StoryImageWidthType;
@@ -25,30 +24,37 @@ type SbStoryImageProps = {
2524

2625
export const SbStoryImage = ({
2726
blok: {
28-
image: { filename, alt, focus } = {},
27+
image: { filename, alt } = {},
2928
caption,
3029
captionAlign,
3130
isCard,
31+
backgroundColor,
3232
imageWidth,
3333
visibleVertical,
34-
backgroundColor,
3534
spacingTop,
3635
spacingBottom,
3736
},
3837
blok,
3938
}: SbStoryImageProps) => {
40-
const Caption = hasRichText(caption) ? <RichText textColor="black-70" wysiwyg={caption} /> : undefined;
39+
const Caption = hasRichText(caption)
40+
? <RichText
41+
textColor="cool-grey"
42+
wysiwyg={caption}
43+
// Edge-to-edge images always have center aligned caption
44+
textAlign={imageWidth !== 'su-w-full' ? captionAlign : 'center'}
45+
/>
46+
: undefined;
4147

4248
return (
4349
<StoryImage
4450
{...storyblokEditable(blok)}
4551
imageSrc={filename}
46-
imageFocus={focus}
4752
imageWidth={imageWidth}
4853
visibleVertical={visibleVertical}
4954
alt={alt}
5055
caption={Caption}
5156
captionAlign={captionAlign}
57+
isCard={isCard}
5258
backgroundColor={backgroundColor}
5359
pt={spacingTop}
5460
pb={spacingBottom}

components/Typography/typography.styles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const textColors = {
6363
default: '', // Inherit from the base
6464
black: 'text-black',
6565
white: 'text-white',
66+
'cool-grey': 'text-cool-grey',
6667
'black-70': 'text-black-70', // For caption
6768
'digital-red': 'text-digital-red',
6869
};

utilities/getImageSources.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { getProcessedImage } from './getProcessedImage';
2+
3+
export type ResponsiveBreakpoint = {
4+
cropWidth: number;
5+
minWidth: number;
6+
};
7+
8+
export type ImageSource = {
9+
srcSet: string;
10+
media: string;
11+
width?: number;
12+
height?: number;
13+
};
14+
15+
// Define standard breakpoints for generating responsive images
16+
const defaultResponsiveBreakpoints: ResponsiveBreakpoint[] = [
17+
{ cropWidth: 2000, minWidth: 1500 },
18+
{ cropWidth: 1500, minWidth: 1200 },
19+
{ cropWidth: 1200, minWidth: 992 },
20+
{ cropWidth: 1000, minWidth: 768 },
21+
{ cropWidth: 800, minWidth: 461 },
22+
{ cropWidth: 460, minWidth: 0 }, // Mobile/smallest size
23+
];
24+
25+
/**
26+
* Generates responsive image sources for use with picture element
27+
*
28+
* @param filename - The image filename from Storyblok
29+
* @param originalWidth - Original width of the image
30+
* @param originalHeight - Original height of the image
31+
* @param imageFocus - Optional focus point for image cropping
32+
* @param customBreakpoints - Optional custom breakpoints to override defaults
33+
* @returns Array of image sources with srcSet and media queries
34+
*/
35+
export const getImageSources = (
36+
filename: string,
37+
originalWidth: number,
38+
originalHeight: number,
39+
imageFocus?: string,
40+
customBreakpoints?: ResponsiveBreakpoint[],
41+
): ImageSource[] => {
42+
const sources: ImageSource[] = [];
43+
const breakpoints = customBreakpoints || defaultResponsiveBreakpoints;
44+
45+
// If the original image width is < 2000px, find out what breakpoint range it falls into
46+
const largestBp = breakpoints.find(bp => originalWidth >= bp.minWidth && originalWidth < bp.cropWidth);
47+
48+
// If we found an appropriate breakpoint, insert the original image at that breakpoint
49+
// For example, if the original image is 1100px, it will be used for the min-width: 992px breakpoint
50+
if (largestBp) {
51+
sources.push({
52+
srcSet: getProcessedImage(filename, '', imageFocus), // Original size
53+
media: `(min-width: ${largestBp.minWidth}px)`,
54+
width: originalWidth,
55+
height: originalHeight,
56+
});
57+
}
58+
59+
// Add all smaller sizes that are relevant
60+
breakpoints
61+
// First pass: always include the mobile size, and keep all the breakpoints with minWidth < the original image width
62+
.filter(bp => bp.cropWidth < originalWidth || bp.cropWidth === 460)
63+
// If the original image is wider than 2000px (no largestBp assigned), keep all the breakpoints from the first pass
64+
// Otherwise, keep only the breakpoints that are smaller than the largestBp
65+
.filter(bp => !largestBp || bp.minWidth < largestBp.minWidth)
66+
.forEach(bp => {
67+
const cropSize = `${bp.cropWidth}x0`;
68+
69+
sources.push({
70+
srcSet: getProcessedImage(filename, cropSize, imageFocus),
71+
// The smaller source uses max-width while the larger uses min-width for the media attribute
72+
media: bp.minWidth > 0 ? `(min-width: ${bp.minWidth}px)` : `(max-width: ${bp.cropWidth}px)`,
73+
width: bp.cropWidth,
74+
height: Math.round(bp.cropWidth * (originalHeight / originalWidth)), // Maintain aspect ratio
75+
});
76+
});
77+
78+
return sources;
79+
};

0 commit comments

Comments
 (0)