Skip to content

Commit

Permalink
feat(loading): image component supports media queries
Browse files Browse the repository at this point in the history
  • Loading branch information
marcolink committed Jan 28, 2022
1 parent 8c2b39f commit e365dc0
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 77 deletions.
146 changes: 77 additions & 69 deletions app/components/contentful-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ declare namespace ContentfulImageTransform {
| 'faces'
type Format = 'jpg' | 'progressive' | 'gif' | 'png' | '8bit' | 'webp' | 'avif'
type Fit = 'pad' | 'fill' | 'scale' | 'crop' | 'thumb'
type Key = 'q' | 'w' | 'h' | 'fit' | 'f' | 'r' | 'fm' | 'fl' | 'bg'
}

type MediaQuery = Record<number, {
export type MediaQuery = {
width?: number,
height?: number,
}>
quality?: number
}

export type MediaQueries = Record<string, MediaQuery>

type Props = {
image: TypeImage // Asset['fields']['file']
image: TypeImage
alt?: string
width?: number,
height?: number,
Expand All @@ -35,19 +39,47 @@ type Props = {
focusArea?: ContentfulImageTransform.FocusArea,
radius?: number
decoding?: 'auto' | 'sync' | 'async',
// to be implemented
// mediaQueries?: MediaQuery
mediaQueries?: MediaQueries
};

const imagePropsMap = {
width: 'w',
height: 'h',
behaviour: 'fit',
quality: 'q',
backgroundColor: 'bg',
focusArea: 'f',
radius: 'r',
format: 'fm'
function buildQueryParam(key: ContentfulImageTransform.Key, value?: string | number): Record<string, string> | null {
if (value) {
return {[key]: value.toString()}
} else {
return null;
}
}

function buildFormatQueryParam(format?: ContentfulImageTransform.Format): Record<string, string> | null {
if (!format) {
return null
} else {
if (format === '8bit') {
return {fm: 'png', fl: 'png8'}
} else if (format === 'progressive') {
return {fm: 'jpg', fl: 'progressive'}
} else {
return {fm: format}
}
}
}

function buildSrcUrl(url: string, query: Record<string, string>) {
return `${url}?${new URLSearchParams(query).toString()}`
}

function buildMimeTime(format?: ContentfulImageTransform.Format) {
if (!format) {
return
}
switch (format) {
case '8bit' :
return 'image/png'
case 'progressive':
return 'image/jpeg'
default:
return `image/${format || 'jpeg'}`
}
}

const ContentfulImage: React.FC<Props> = (
Expand All @@ -62,71 +94,47 @@ const ContentfulImage: React.FC<Props> = (
focusArea,
format,
radius,
decoding = 'auto'
decoding = 'auto',
mediaQueries = {}
}) => {

let mimeType = image.contentType // extract mimeType from image.url
let query = {};

// should only allow ContentfulImage.Query props
function addToQuery(prop: Record<string, string>) {
query = {...query, ...prop}
}

if (quality) {
addToQuery({[imagePropsMap['quality']]: quality.toString()})
let mimeType = buildMimeTime(format) || image.contentType

const query = {
...buildQueryParam('q', quality),
...buildQueryParam('w', width),
...buildQueryParam('h', height),
...buildQueryParam('fit', behaviour),
...buildQueryParam('f', focusArea),
...buildQueryParam('r', radius),
...buildQueryParam('bg', backgroundColor),
...buildFormatQueryParam(format),
}

if (width) {
addToQuery({[imagePropsMap['width']]: width.toString()})
}

if (height) {
addToQuery({[imagePropsMap['height']]: height.toString()})
}

if (behaviour) {
addToQuery({[imagePropsMap['behaviour']]: behaviour.toString()})
}

if (focusArea) {
addToQuery({[imagePropsMap['focusArea']]: focusArea.toString()})
}

if (radius) {
addToQuery({[imagePropsMap['radius']]: radius.toString()})
}

if (format) {
if (format === '8bit') {
addToQuery({'fm': 'png'})
addToQuery({'fl': 'png8'})
mimeType = 'image/png'
} else if (format === 'progressive') {
addToQuery({'fm': 'jpg'})
addToQuery({'fl': 'progressive'})
mimeType = 'image/jpg'
} else {
addToQuery({[imagePropsMap['format']]: format.toString()})
mimeType = `image/${format}`
const mediaQuerySources = Object.keys(mediaQueries).map((key: string) => {
const sourceQuery = {
...query,
...buildQueryParam('w', mediaQueries[key].width),
...buildQueryParam('h', mediaQueries[key].height),
}
}

if (quality) {
addToQuery({[imagePropsMap['quality']]: quality.toString()})
}

if (backgroundColor) {
addToQuery({[imagePropsMap['backgroundColor']]: backgroundColor.toString()})
}
return (
<source
srcSet={buildSrcUrl(image.url, sourceQuery)}
type={`${mimeType}`}
media={key}
key={key}
/>
)
})

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

return (
<picture>
<source srcSet={transformSrc} type={`${mimeType}`}/>
{mediaQuerySources}
<source srcSet={src} type={`${mimeType}`}/>
<img
src={image.url}
src={src}
alt={alt || image.title}
decoding={decoding}
width={width}
Expand Down
23 changes: 15 additions & 8 deletions app/components/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import {ContentfulImage} from "~/components/contentful-image";
import {ContentfulImage, MediaQueries} from "~/components/contentful-image";
import {CssModuleWrapper} from "~/components/css-module-wrapper";
import {TypeImage} from "../../types/contentful-graphql-types";

Expand All @@ -9,18 +9,25 @@ type Props = {
content?: React.ReactNode
}

const mediaQueries: MediaQueries = {
"(min-width: 1200px)": {width: 1900},
"(orientation: landscape) (min-width: 800px)": {width: 800},
"(min-width: 400px)": {width: 800},
}

const Hero: React.FC<Props> = ({image, title, content}) => (
<CssModuleWrapper className={"hero-module"}>
<section className={"hero"}>
{image && (
<figure className={'image'}>
<ContentfulImage
alt={title}
image={image}
quality={50}
format={'avif'}
width={1920}
/>
<ContentfulImage
mediaQueries={mediaQueries}
alt={title}
image={image}
quality={50}
format={'avif'}
width={1920}
/>
</figure>
)}
<div className={"details"}>
Expand Down

0 comments on commit e365dc0

Please sign in to comment.