252 lines
6.3 KiB
TypeScript
252 lines
6.3 KiB
TypeScript
import React from 'react';
|
|
import Image from 'next/image';
|
|
import { cn } from '../../lib/utils';
|
|
import { getViewport, generateImageSizes, getOptimalImageQuality } from '../../lib/responsive';
|
|
|
|
// Aspect ratio options
|
|
type AspectRatio = '1:1' | '4:3' | '16:9' | '21:9' | 'auto';
|
|
|
|
// Size options
|
|
type ImageSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
|
|
interface FeaturedImageProps {
|
|
src: string;
|
|
alt: string;
|
|
width?: number;
|
|
height?: number;
|
|
aspectRatio?: AspectRatio;
|
|
size?: ImageSize;
|
|
caption?: string;
|
|
priority?: boolean;
|
|
className?: string;
|
|
objectFit?: 'cover' | 'contain' | 'fill';
|
|
lazy?: boolean;
|
|
// Responsive props
|
|
responsiveSrc?: {
|
|
mobile?: string;
|
|
tablet?: string;
|
|
desktop?: string;
|
|
};
|
|
// Quality optimization
|
|
quality?: number | 'auto';
|
|
// Placeholder
|
|
placeholder?: 'blur' | 'empty';
|
|
blurDataURL?: string;
|
|
}
|
|
|
|
// Helper function to get aspect ratio classes
|
|
const getAspectRatio = (ratio: AspectRatio) => {
|
|
switch (ratio) {
|
|
case '1:1':
|
|
return 'aspect-square';
|
|
case '4:3':
|
|
return 'aspect-[4/3]';
|
|
case '16:9':
|
|
return 'aspect-video';
|
|
case '21:9':
|
|
return 'aspect-[21/9]';
|
|
case 'auto':
|
|
return 'aspect-auto';
|
|
default:
|
|
return 'aspect-auto';
|
|
}
|
|
};
|
|
|
|
// Helper function to get size classes
|
|
const getSizeStyles = (size: ImageSize) => {
|
|
switch (size) {
|
|
case 'sm':
|
|
return 'max-w-xs';
|
|
case 'md':
|
|
return 'max-w-md';
|
|
case 'lg':
|
|
return 'max-w-lg';
|
|
case 'xl':
|
|
return 'max-w-xl';
|
|
case 'full':
|
|
return 'max-w-full';
|
|
default:
|
|
return 'max-w-lg';
|
|
}
|
|
};
|
|
|
|
export const FeaturedImage: React.FC<FeaturedImageProps> = ({
|
|
src,
|
|
alt,
|
|
width,
|
|
height,
|
|
aspectRatio = 'auto',
|
|
size = 'md',
|
|
caption,
|
|
priority = false,
|
|
className = '',
|
|
objectFit = 'cover',
|
|
lazy = true,
|
|
responsiveSrc,
|
|
quality = 'auto',
|
|
placeholder = 'empty',
|
|
blurDataURL,
|
|
}) => {
|
|
const hasDimensions = width && height;
|
|
const shouldLazyLoad = !priority && lazy;
|
|
|
|
// Get responsive image source
|
|
const getResponsiveSrc = () => {
|
|
if (responsiveSrc) {
|
|
if (typeof window === 'undefined') return responsiveSrc.mobile || src;
|
|
|
|
const viewport = getViewport();
|
|
if (viewport.isMobile && responsiveSrc.mobile) return responsiveSrc.mobile;
|
|
if (viewport.isTablet && responsiveSrc.tablet) return responsiveSrc.tablet;
|
|
if (viewport.isDesktop && responsiveSrc.desktop) return responsiveSrc.desktop;
|
|
}
|
|
return src;
|
|
};
|
|
|
|
// Get optimal quality
|
|
const getQuality = () => {
|
|
if (quality === 'auto') {
|
|
if (typeof window === 'undefined') return 75;
|
|
const viewport = getViewport();
|
|
return getOptimalImageQuality(viewport);
|
|
}
|
|
return quality;
|
|
};
|
|
|
|
// Generate responsive sizes attribute
|
|
const getSizes = () => {
|
|
const baseSizes = generateImageSizes();
|
|
|
|
// Adjust based on component size prop
|
|
switch (size) {
|
|
case 'sm':
|
|
return '(max-width: 640px) 50vw, (max-width: 768px) 33vw, 25vw';
|
|
case 'md':
|
|
return '(max-width: 640px) 75vw, (max-width: 768px) 50vw, 33vw';
|
|
case 'lg':
|
|
return baseSizes;
|
|
case 'xl':
|
|
return '(max-width: 640px) 100vw, (max-width: 768px) 75vw, 50vw';
|
|
case 'full':
|
|
return '100vw';
|
|
default:
|
|
return baseSizes;
|
|
}
|
|
};
|
|
|
|
const responsiveImageSrc = getResponsiveSrc();
|
|
const optimalQuality = getQuality();
|
|
|
|
return (
|
|
<figure className={cn('relative', getSizeStyles(size), className)}>
|
|
<div className={cn(
|
|
'relative overflow-hidden rounded-lg',
|
|
getAspectRatio(aspectRatio),
|
|
// Ensure container has dimensions if aspect ratio is specified
|
|
aspectRatio !== 'auto' && 'w-full',
|
|
// Mobile-optimized border radius
|
|
'sm:rounded-lg'
|
|
)}>
|
|
<Image
|
|
src={responsiveImageSrc}
|
|
alt={alt}
|
|
width={hasDimensions ? width : undefined}
|
|
height={hasDimensions ? height : undefined}
|
|
fill={!hasDimensions}
|
|
priority={priority}
|
|
loading={shouldLazyLoad ? 'lazy' : 'eager'}
|
|
quality={optimalQuality}
|
|
placeholder={placeholder}
|
|
blurDataURL={blurDataURL}
|
|
className={cn(
|
|
'transition-transform duration-300 ease-in-out',
|
|
objectFit === 'cover' && 'object-cover',
|
|
objectFit === 'contain' && 'object-contain',
|
|
objectFit === 'fill' && 'object-fill',
|
|
// Smooth scaling on mobile, more pronounced on desktop
|
|
'active:scale-95 md:hover:scale-105',
|
|
// Ensure no layout shift
|
|
'bg-gray-100'
|
|
)}
|
|
sizes={getSizes()}
|
|
// Add loading optimization
|
|
fetchPriority={priority ? 'high' : 'low'}
|
|
/>
|
|
</div>
|
|
|
|
{caption && (
|
|
<figcaption className={cn(
|
|
'mt-2 text-sm text-gray-600',
|
|
'text-center italic',
|
|
// Mobile-optimized text size
|
|
'text-xs sm:text-sm'
|
|
)}>
|
|
{caption}
|
|
</figcaption>
|
|
)}
|
|
</figure>
|
|
);
|
|
};
|
|
|
|
// Sub-components for common image patterns
|
|
export const Avatar: React.FC<{
|
|
src: string;
|
|
alt: string;
|
|
size?: 'sm' | 'md' | 'lg';
|
|
className?: string;
|
|
}> = ({ src, alt, size = 'md', className = '' }) => {
|
|
const sizeClasses = {
|
|
sm: 'w-8 h-8',
|
|
md: 'w-12 h-12',
|
|
lg: 'w-16 h-16',
|
|
}[size];
|
|
|
|
return (
|
|
<div className={cn(
|
|
'relative overflow-hidden rounded-full',
|
|
sizeClasses,
|
|
className
|
|
)}>
|
|
<Image
|
|
src={src}
|
|
alt={alt}
|
|
fill
|
|
className="object-cover"
|
|
sizes={`${sizeClasses}`}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const ImageGallery: React.FC<{
|
|
images: Array<{
|
|
src: string;
|
|
alt: string;
|
|
caption?: string;
|
|
}>;
|
|
cols?: 2 | 3 | 4;
|
|
className?: string;
|
|
}> = ({ images, cols = 3, className = '' }) => {
|
|
const colClasses = {
|
|
2: 'grid-cols-1 md:grid-cols-2',
|
|
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
|
}[cols];
|
|
|
|
return (
|
|
<div className={cn('grid gap-4', colClasses, className)}>
|
|
{images.map((image, index) => (
|
|
<FeaturedImage
|
|
key={index}
|
|
src={image.src}
|
|
alt={image.alt}
|
|
caption={image.caption}
|
|
size="full"
|
|
aspectRatio="4:3"
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FeaturedImage; |