UNPKG

@ai-growth/nextjs

Version:

Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering

235 lines (234 loc) 8.54 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThumbnailImage = exports.CardImage = exports.AvatarImage = exports.HeroImage = exports.ResponsiveImage = exports.CmsImage = exports.OptimizedImage = void 0; const jsx_runtime_1 = require("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. */ const react_1 = require("react"); const image_1 = __importDefault(require("next/image")); const image_processing_1 = require("../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 (0, image_processing_1.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 = (0, image_processing_1.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 */ 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] = (0, react_1.useState)(false); const [isLoading, setIsLoading] = (0, react_1.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 ((0, jsx_runtime_1.jsx)("div", { className: `${className || ''} bg-gray-200 flex items-center justify-center`, style: { width, height }, children: (0, jsx_runtime_1.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 ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(image_1.default, { ...imageProps }), isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 bg-gray-200 animate-pulse", style: { width, height } }))] })); }; exports.OptimizedImage = OptimizedImage; /** * CMS-specific image component with enhanced Sanity integration */ const CmsImage = ({ image, fallbackSrc = '/placeholder-image.jpg', showFallback = true, alt, ...props }) => { const [hasError, setHasError] = (0, react_1.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 ((0, jsx_runtime_1.jsx)(exports.OptimizedImage, { image: fallbackSrc, alt: altToUse, onError: handleError, ...props })); } return ((0, jsx_runtime_1.jsx)(exports.OptimizedImage, { image: imageToUse, alt: altToUse, onError: handleError, ...props })); }; exports.CmsImage = CmsImage; /** * Responsive image component with breakpoint handling */ 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 ((0, jsx_runtime_1.jsx)(exports.OptimizedImage, { ...props, height: finalHeight, sizes: responsiveSizes })); }; exports.ResponsiveImage = ResponsiveImage; /** * Hero image component optimized for above-the-fold content */ const HeroImage = (props) => ((0, jsx_runtime_1.jsx)(exports.ResponsiveImage, { ...props, priority: true, loading: "eager", quality: 90, aspectRatio: 21 / 9, breakpoints: { mobile: 640, tablet: 1024, desktop: 1920, large: 2560 } })); exports.HeroImage = HeroImage; /** * Avatar image component for user profiles */ 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 ((0, jsx_runtime_1.jsx)(exports.OptimizedImage, { ...props, width: dimension, height: dimension, className: `${roundedClass} ${className}`, objectFit: objectFit, quality: 85 })); }; exports.AvatarImage = AvatarImage; /** * Card image component for content previews */ const CardImage = (props) => ((0, jsx_runtime_1.jsx)(exports.ResponsiveImage, { ...props, aspectRatio: 4 / 3, quality: 80, breakpoints: { mobile: 320, tablet: 400, desktop: 600, large: 800 } })); exports.CardImage = CardImage; /** * Thumbnail image component for galleries and lists */ const ThumbnailImage = (props) => ((0, jsx_runtime_1.jsx)(exports.OptimizedImage, { ...props, width: 200, height: 200, quality: 75, sizes: "(max-width: 768px) 150px, 200px" })); exports.ThumbnailImage = ThumbnailImage; // Default export exports.default = exports.OptimizedImage;