UNPKG

@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
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;