Skip to content

Commit e365dc0

Browse files
committed
feat(loading): image component supports media queries
1 parent 8c2b39f commit e365dc0

File tree

2 files changed

+92
-77
lines changed

2 files changed

+92
-77
lines changed

app/components/contentful-image.tsx

Lines changed: 77 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ declare namespace ContentfulImageTransform {
1616
| 'faces'
1717
type Format = 'jpg' | 'progressive' | 'gif' | 'png' | '8bit' | 'webp' | 'avif'
1818
type Fit = 'pad' | 'fill' | 'scale' | 'crop' | 'thumb'
19+
type Key = 'q' | 'w' | 'h' | 'fit' | 'f' | 'r' | 'fm' | 'fl' | 'bg'
1920
}
2021

21-
type MediaQuery = Record<number, {
22+
export type MediaQuery = {
2223
width?: number,
2324
height?: number,
24-
}>
25+
quality?: number
26+
}
27+
28+
export type MediaQueries = Record<string, MediaQuery>
2529

2630
type Props = {
27-
image: TypeImage // Asset['fields']['file']
31+
image: TypeImage
2832
alt?: string
2933
width?: number,
3034
height?: number,
@@ -35,19 +39,47 @@ type Props = {
3539
focusArea?: ContentfulImageTransform.FocusArea,
3640
radius?: number
3741
decoding?: 'auto' | 'sync' | 'async',
38-
// to be implemented
39-
// mediaQueries?: MediaQuery
42+
mediaQueries?: MediaQueries
4043
};
4144

42-
const imagePropsMap = {
43-
width: 'w',
44-
height: 'h',
45-
behaviour: 'fit',
46-
quality: 'q',
47-
backgroundColor: 'bg',
48-
focusArea: 'f',
49-
radius: 'r',
50-
format: 'fm'
45+
function buildQueryParam(key: ContentfulImageTransform.Key, value?: string | number): Record<string, string> | null {
46+
if (value) {
47+
return {[key]: value.toString()}
48+
} else {
49+
return null;
50+
}
51+
}
52+
53+
function buildFormatQueryParam(format?: ContentfulImageTransform.Format): Record<string, string> | null {
54+
if (!format) {
55+
return null
56+
} else {
57+
if (format === '8bit') {
58+
return {fm: 'png', fl: 'png8'}
59+
} else if (format === 'progressive') {
60+
return {fm: 'jpg', fl: 'progressive'}
61+
} else {
62+
return {fm: format}
63+
}
64+
}
65+
}
66+
67+
function buildSrcUrl(url: string, query: Record<string, string>) {
68+
return `${url}?${new URLSearchParams(query).toString()}`
69+
}
70+
71+
function buildMimeTime(format?: ContentfulImageTransform.Format) {
72+
if (!format) {
73+
return
74+
}
75+
switch (format) {
76+
case '8bit' :
77+
return 'image/png'
78+
case 'progressive':
79+
return 'image/jpeg'
80+
default:
81+
return `image/${format || 'jpeg'}`
82+
}
5183
}
5284

5385
const ContentfulImage: React.FC<Props> = (
@@ -62,71 +94,47 @@ const ContentfulImage: React.FC<Props> = (
6294
focusArea,
6395
format,
6496
radius,
65-
decoding = 'auto'
97+
decoding = 'auto',
98+
mediaQueries = {}
6699
}) => {
67100

68-
let mimeType = image.contentType // extract mimeType from image.url
69-
let query = {};
70-
71-
// should only allow ContentfulImage.Query props
72-
function addToQuery(prop: Record<string, string>) {
73-
query = {...query, ...prop}
74-
}
75-
76-
if (quality) {
77-
addToQuery({[imagePropsMap['quality']]: quality.toString()})
101+
let mimeType = buildMimeTime(format) || image.contentType
102+
103+
const query = {
104+
...buildQueryParam('q', quality),
105+
...buildQueryParam('w', width),
106+
...buildQueryParam('h', height),
107+
...buildQueryParam('fit', behaviour),
108+
...buildQueryParam('f', focusArea),
109+
...buildQueryParam('r', radius),
110+
...buildQueryParam('bg', backgroundColor),
111+
...buildFormatQueryParam(format),
78112
}
79113

80-
if (width) {
81-
addToQuery({[imagePropsMap['width']]: width.toString()})
82-
}
83-
84-
if (height) {
85-
addToQuery({[imagePropsMap['height']]: height.toString()})
86-
}
87-
88-
if (behaviour) {
89-
addToQuery({[imagePropsMap['behaviour']]: behaviour.toString()})
90-
}
91-
92-
if (focusArea) {
93-
addToQuery({[imagePropsMap['focusArea']]: focusArea.toString()})
94-
}
95-
96-
if (radius) {
97-
addToQuery({[imagePropsMap['radius']]: radius.toString()})
98-
}
99-
100-
if (format) {
101-
if (format === '8bit') {
102-
addToQuery({'fm': 'png'})
103-
addToQuery({'fl': 'png8'})
104-
mimeType = 'image/png'
105-
} else if (format === 'progressive') {
106-
addToQuery({'fm': 'jpg'})
107-
addToQuery({'fl': 'progressive'})
108-
mimeType = 'image/jpg'
109-
} else {
110-
addToQuery({[imagePropsMap['format']]: format.toString()})
111-
mimeType = `image/${format}`
114+
const mediaQuerySources = Object.keys(mediaQueries).map((key: string) => {
115+
const sourceQuery = {
116+
...query,
117+
...buildQueryParam('w', mediaQueries[key].width),
118+
...buildQueryParam('h', mediaQueries[key].height),
112119
}
113-
}
114-
115-
if (quality) {
116-
addToQuery({[imagePropsMap['quality']]: quality.toString()})
117-
}
118-
119-
if (backgroundColor) {
120-
addToQuery({[imagePropsMap['backgroundColor']]: backgroundColor.toString()})
121-
}
120+
return (
121+
<source
122+
srcSet={buildSrcUrl(image.url, sourceQuery)}
123+
type={`${mimeType}`}
124+
media={key}
125+
key={key}
126+
/>
127+
)
128+
})
122129

123-
const transformSrc = `${image.url}?${new URLSearchParams(query).toString()}`
130+
const src = buildSrcUrl(image.url, query);
124131

125132
return (
126133
<picture>
127-
<source srcSet={transformSrc} type={`${mimeType}`}/>
134+
{mediaQuerySources}
135+
<source srcSet={src} type={`${mimeType}`}/>
128136
<img
129-
src={image.url}
137+
src={src}
130138
alt={alt || image.title}
131139
decoding={decoding}
132140
width={width}

app/components/hero.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import {ContentfulImage} from "~/components/contentful-image";
2+
import {ContentfulImage, MediaQueries} from "~/components/contentful-image";
33
import {CssModuleWrapper} from "~/components/css-module-wrapper";
44
import {TypeImage} from "../../types/contentful-graphql-types";
55

@@ -9,18 +9,25 @@ type Props = {
99
content?: React.ReactNode
1010
}
1111

12+
const mediaQueries: MediaQueries = {
13+
"(min-width: 1200px)": {width: 1900},
14+
"(orientation: landscape) (min-width: 800px)": {width: 800},
15+
"(min-width: 400px)": {width: 800},
16+
}
17+
1218
const Hero: React.FC<Props> = ({image, title, content}) => (
1319
<CssModuleWrapper className={"hero-module"}>
1420
<section className={"hero"}>
1521
{image && (
1622
<figure className={'image'}>
17-
<ContentfulImage
18-
alt={title}
19-
image={image}
20-
quality={50}
21-
format={'avif'}
22-
width={1920}
23-
/>
23+
<ContentfulImage
24+
mediaQueries={mediaQueries}
25+
alt={title}
26+
image={image}
27+
quality={50}
28+
format={'avif'}
29+
width={1920}
30+
/>
2431
</figure>
2532
)}
2633
<div className={"details"}>

0 commit comments

Comments
 (0)