Files
shop-server/client/src/shared/ui/OptimizedImage.tsx
T
2026-05-26 12:10:38 +05:00

83 lines
2.5 KiB
TypeScript

import * as React from 'react'
import { useMemo } from 'react'
import Box from '@mui/material/Box'
import type { SxProps, Theme } from '@mui/material/styles'
const DEFAULT_WIDTHS = [320, 640, 1024, 1600]
type OptimizedImageProps = {
src: string
alt: string
widths?: number[]
sizes?: string
sx?: SxProps<Theme>
priority?: boolean
}
/** Extract UUID and subdir from a /uploads/... URL */
function parseUploadUrl(src: string): { uuid: string; ext: string; subdir: string } | null {
const match = src.match(/^\/uploads(?:\/(reviews))?\/([^.\\/]+)\.(png|jpe?g|webp)/i)
if (!match) return null
return { subdir: match[1] || '', uuid: match[2], ext: match[3].toLowerCase() }
}
function buildSrcSet(src: string, widths: number[]): string | null {
const parsed = parseUploadUrl(src)
if (!parsed) return null
const pathPrefix = parsed.subdir ? `${parsed.subdir}/` : ''
return widths.map((w) => `/uploads-resized/${pathPrefix}${parsed.uuid}.avif?w=${w} ${w}w`).join(', ')
}
function buildFallbackSrc(src: string, width: number): string {
const parsed = parseUploadUrl(src)
if (!parsed) return src
const pathPrefix = parsed.subdir ? `${parsed.subdir}/` : ''
return `/uploads-resized/${pathPrefix}${parsed.uuid}.webp?w=${width}`
}
export const OptimizedImage = React.memo(function OptimizedImage({
src,
alt,
widths = DEFAULT_WIDTHS,
sizes,
sx,
priority = false,
}: OptimizedImageProps) {
const srcSet = useMemo(() => buildSrcSet(src, widths), [src, widths])
const fallbackSrc = useMemo(() => buildFallbackSrc(src, widths[0]), [src, widths])
// If src is not an upload URL, render a plain img
if (!srcSet) {
return (
<Box
component="img"
src={src}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
fetchPriority={priority ? 'high' : undefined}
decoding="async"
sx={sx}
/>
)
}
const sizesAttr = sizes ?? '(max-width: 600px) 320px, (max-width: 1024px) 640px, 1024px'
return (
<Box component="picture" sx={{ display: 'block', lineHeight: 0, ...sx }}>
<Box component="source" type="image/avif" srcSet={srcSet} sizes={sizesAttr} />
<Box component="source" type="image/webp" srcSet={fallbackSrc} sizes={sizesAttr} />
<Box
component="img"
src={fallbackSrc}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
fetchPriority={priority ? 'high' : undefined}
decoding="async"
sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</Box>
)
})