Files
klz-cables.com/components/content/FeaturedImage.tsx
2025-12-29 18:18:48 +01:00

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;