@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
222 lines (221 loc) • 7.58 kB
JavaScript
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
/**
* @fileoverview Optimized Image Components for Next.js
*
* This module provides React components that use Next.js Image optimization
* with Sanity CMS integration, responsive sizing, and performance features.
*/
import { useState } from 'react';
import Image from 'next/image';
import { buildSanityImageUrl } from '../utils/seo/image-processing';
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Create a Sanity image loader for Next.js Image
*/
const sanityLoader = ({ src, width, quality = 80 }) => {
// Handle full URLs
if (src.startsWith('http')) {
if (src.includes('cdn.sanity.io')) {
const url = new URL(src);
url.searchParams.set('w', width.toString());
url.searchParams.set('q', quality.toString());
url.searchParams.set('auto', 'format');
return url.toString();
}
return src;
}
// Handle Sanity asset references
if (src.startsWith('image-')) {
return buildSanityImageUrl(src, {
width,
quality,
format: 'auto'
});
}
return src;
};
/**
* Process Sanity image to get optimized source URL
*/
function processSanityImage(image, width, height, quality = 80) {
// Handle null/undefined
if (!image) {
return {
src: '/placeholder-image.jpg',
alt: 'Placeholder image'
};
}
// Handle string URLs
if (typeof image === 'string') {
return {
src: image,
alt: 'Image'
};
}
// Handle Sanity image objects
if (image.asset?._ref) {
const src = buildSanityImageUrl(image.asset._ref, {
width,
height,
fit: 'crop',
format: 'auto',
quality
});
return {
src,
alt: image.alt || 'Image'
};
}
// Fallback
return {
src: '/placeholder-image.jpg',
alt: 'Image'
};
}
/**
* Generate responsive sizes attribute
*/
function generateSizes(breakpoints) {
if (!breakpoints) {
return '100vw';
}
const sizes = [];
if (breakpoints.large) {
sizes.push(`(min-width: 1280px) ${breakpoints.large}px`);
}
if (breakpoints.desktop) {
sizes.push(`(min-width: 1024px) ${breakpoints.desktop}px`);
}
if (breakpoints.tablet) {
sizes.push(`(min-width: 768px) ${breakpoints.tablet}px`);
}
if (breakpoints.mobile) {
sizes.push(`${breakpoints.mobile}px`);
}
return sizes.join(', ') || '100vw';
}
// ============================================================================
// OPTIMIZED IMAGE COMPONENTS
// ============================================================================
/**
* Base optimized image component using Next.js Image
*/
export const OptimizedImage = ({ image, alt, width, height, className, priority = false, quality = 80, placeholder = 'empty', blurDataURL, sizes = '100vw', objectFit = 'cover', objectPosition = 'center', onLoad, onError, ...props }) => {
const [hasError, setHasError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { src, alt: processedAlt } = processSanityImage(image, width, height, quality);
const finalAlt = alt || processedAlt;
const handleLoad = () => {
setIsLoading(false);
onLoad?.();
};
const handleError = () => {
setHasError(true);
setIsLoading(false);
onError?.();
};
// Show placeholder for error state
if (hasError) {
return (_jsx("div", { className: `${className || ''} bg-gray-200 flex items-center justify-center`, style: { width, height }, children: _jsx("span", { className: "text-gray-500 text-sm", children: "Image not available" }) }));
}
const imageProps = {
src,
alt: finalAlt,
width,
height,
className,
priority,
quality,
sizes,
loader: sanityLoader,
onLoad: handleLoad,
onError: handleError,
style: {
objectFit,
objectPosition,
},
...props
};
// Add placeholder props
if (placeholder === 'blur' && blurDataURL) {
imageProps.placeholder = 'blur';
imageProps.blurDataURL = blurDataURL;
}
return (_jsxs(_Fragment, { children: [_jsx(Image, { ...imageProps }), isLoading && (_jsx("div", { className: "absolute inset-0 bg-gray-200 animate-pulse", style: { width, height } }))] }));
};
/**
* CMS-specific image component with enhanced Sanity integration
*/
export const CmsImage = ({ image, fallbackSrc = '/placeholder-image.jpg', showFallback = true, alt, ...props }) => {
const [hasError, setHasError] = useState(false);
// Use fallback if no image provided
const imageToUse = image || (showFallback ? fallbackSrc : null);
const altToUse = alt || image?.alt || 'CMS Image';
const handleError = () => {
setHasError(true);
props.onError?.();
};
// Show fallback for error or missing image
if (hasError || !imageToUse) {
if (!showFallback) {
return null;
}
return (_jsx(OptimizedImage, { image: fallbackSrc, alt: altToUse, onError: handleError, ...props }));
}
return (_jsx(OptimizedImage, { image: imageToUse, alt: altToUse, onError: handleError, ...props }));
};
/**
* Responsive image component with breakpoint handling
*/
export const ResponsiveImage = ({ breakpoints = {
mobile: 640,
tablet: 1024,
desktop: 1280,
large: 1920
}, aspectRatio, height: providedHeight, ...props }) => {
// Calculate height from aspect ratio if provided
const calculatedHeight = aspectRatio ? props.width / aspectRatio : providedHeight;
const finalHeight = calculatedHeight || props.width; // Square fallback
const responsiveSizes = generateSizes(breakpoints);
return (_jsx(OptimizedImage, { ...props, height: finalHeight, sizes: responsiveSizes }));
};
/**
* Hero image component optimized for above-the-fold content
*/
export const HeroImage = (props) => (_jsx(ResponsiveImage, { ...props, priority: true, loading: "eager", quality: 90, aspectRatio: 21 / 9, breakpoints: {
mobile: 640,
tablet: 1024,
desktop: 1920,
large: 2560
} }));
/**
* Avatar image component for user profiles
*/
export const AvatarImage = ({ size = 'md', className = '', objectFit = 'cover', ...props }) => {
const sizeMap = {
sm: 40,
md: 64,
lg: 96,
xl: 128
};
const dimension = sizeMap[size];
const roundedClass = 'rounded-full';
return (_jsx(OptimizedImage, { ...props, width: dimension, height: dimension, className: `${roundedClass} ${className}`, objectFit: objectFit, quality: 85 }));
};
/**
* Card image component for content previews
*/
export const CardImage = (props) => (_jsx(ResponsiveImage, { ...props, aspectRatio: 4 / 3, quality: 80, breakpoints: {
mobile: 320,
tablet: 400,
desktop: 600,
large: 800
} }));
/**
* Thumbnail image component for galleries and lists
*/
export const ThumbnailImage = (props) => (_jsx(OptimizedImage, { ...props, width: 200, height: 200, quality: 75, sizes: "(max-width: 768px) 150px, 200px" }));
// Default export
export default OptimizedImage;