migration wip
This commit is contained in:
252
components/content/FeaturedImage.tsx
Normal file
252
components/content/FeaturedImage.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user