@shopify/shop-minis-react
Version:
React component library for Shopify Shop Minis with Tailwind CSS v4 support (source-only, requires TypeScript)
130 lines (109 loc) • 3.7 kB
text/typescript
import {toUint8Array} from 'js-base64'
import {thumbHashToDataURL} from 'thumbhash'
/**
* Converts a thumbhash string to a data URL for use as an image placeholder
* @param thumbhash Base64 encoded thumbhash string
* @returns Data URL that can be used as image source or undefined if conversion fails
*/
export function getThumbhashDataURL(thumbhash?: string): string | undefined {
if (!thumbhash) return
try {
const thumbhashArray = toUint8Array(thumbhash)
return thumbHashToDataURL(thumbhashArray)
} catch (error) {
console.warn('Failed to decode thumbhash to data URL', error)
return undefined
}
}
/**
* Converts a data URL to a Blob
* @param dataURL The data URL to convert
* @returns A Blob object
*/
export function dataURLToBlob(dataURL: string): Blob {
const [header, base64Data] = dataURL.split(',')
const mimeMatch = header.match(/:(.*?);/)
const mime = mimeMatch ? mimeMatch[1] : 'image/png'
const binary = atob(base64Data)
const array = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i)
}
return new Blob([array], {type: mime})
}
/**
* Converts a thumbhash string to a blob URL for use as an image placeholder
* This is useful when CSP restrictions prevent data URLs
* @param thumbhash Base64 encoded thumbhash string
* @returns Blob URL that can be used as image source or undefined if conversion fails
*/
export function getThumbhashBlobURL(thumbhash?: string): string | undefined {
if (!thumbhash) return
try {
const dataURL = getThumbhashDataURL(thumbhash)
if (!dataURL) return
const blob = dataURLToBlob(dataURL)
return URL.createObjectURL(blob)
} catch (error) {
console.warn('Failed to create thumbhash blob URL', error)
return undefined
}
}
/** Converts a file to a data URI
* @param file The file to convert
* @returns A promise that resolves to the data URI string
*/
export function fileToDataUri(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
const ImageSizes = {
xxsUrl: 32,
xsUrl: 64,
sUrl: 128,
xxsmUrl: 256,
xsmUrl: 384,
smUrl: 512,
mUrl: 640,
lUrl: 1080,
xlUrl: 2048,
} as const
type Key = keyof typeof ImageSizes
/**
* Acceptable offset for image sizes. An image could use the size that is within this offset.
*/
const offsetPercentage = 0.05
const sortedImageSizes = Object.entries(ImageSizes).sort(
([, firstSize], [, secondSize]) => firstSize - secondSize
)
const getImageSizeKeyWithSize = (size: number): Key => {
for (const [key, imgSize] of sortedImageSizes) {
const upperBoundSize = imgSize + imgSize * offsetPercentage
if (size <= upperBoundSize) return key as Key
}
return 'xlUrl'
}
const resizeImage = (imageUrl: string, width: number) => {
const pattern = new RegExp(/\?+/g)
const delimiter = pattern.test(imageUrl) ? '&' : '?'
return `${imageUrl}${delimiter}width=${width}`
}
/**
* Optimizes Shopify CDN image URLs by adding a width parameter based on screen size
* @param url The image URL to optimize
* @returns The optimized URL with width parameter if it's a Shopify CDN image, otherwise returns the original URL
*/
export const getResizedImageUrl = (url?: string): string => {
if (!url) return ''
// Only process Shopify CDN images
if (!url.startsWith('https://cdn.shopify.com')) {
return url
}
const width = window.innerWidth ?? screen.width
const key = getImageSizeKeyWithSize(width)
return resizeImage(url, ImageSizes[key])
}