UNPKG

@ai-growth/nextjs

Version:

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

304 lines (302 loc) 14.5 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { createContext, useContext, useState, useCallback, useEffect } from 'react'; // ============================================================================ // LOADING CONTEXT // ============================================================================ const LoadingContext = createContext(null); /** * Provider for managing global and operation-specific loading states */ export const LoadingProvider = ({ children, initialLoading = false, }) => { const [isLoading, setIsLoading] = useState(initialLoading); const [loadingStates, setLoadingStates] = useState({}); const setLoading = useCallback((loading) => { setIsLoading(loading); }, []); const setOperationLoading = useCallback((operation, loading) => { setLoadingStates(prev => ({ ...prev, [operation]: loading, })); }, []); const isOperationLoading = useCallback((operation) => { return loadingStates[operation] || false; }, [loadingStates]); const contextValue = { isLoading, setLoading, loadingStates, setOperationLoading, isOperationLoading, }; return (_jsx(LoadingContext.Provider, { value: contextValue, children: children })); }; /** * Hook to access loading context */ export const useLoading = () => { const context = useContext(LoadingContext); if (!context) { throw new Error('useLoading must be used within a LoadingProvider'); } return context; }; /** * Hook for managing operation-specific loading states */ export const useOperationLoading = (operationName) => { const { isOperationLoading, setOperationLoading } = useLoading(); const isLoading = isOperationLoading(operationName); const setLoading = useCallback((loading) => { setOperationLoading(operationName, loading); }, [operationName, setOperationLoading]); return { isLoading, setLoading }; }; // ============================================================================ // BASE SKELETON COMPONENT // ============================================================================ /** * Base skeleton component with customizable appearance and animations */ export const Skeleton = ({ width = '100%', height = '1rem', borderRadius = '4px', className = '', style = {}, animation = 'pulse', animationDuration = 1.5, backgroundColor = '#f0f0f0', highlightColor = '#e0e0e0', visible = true, }) => { if (!visible) { return null; } const getAnimationStyles = () => { if (animation === 'none') { return {}; } const baseAnimation = { animationDuration: `${animationDuration}s`, animationIterationCount: 'infinite', animationTimingFunction: 'ease-in-out', }; if (animation === 'pulse') { return { ...baseAnimation, animationName: 'skeleton-pulse', }; } if (animation === 'wave') { return { ...baseAnimation, animationName: 'skeleton-wave', background: `linear-gradient(90deg, ${backgroundColor} 25%, ${highlightColor} 50%, ${backgroundColor} 75%)`, backgroundSize: '200% 100%', }; } return {}; }; const skeletonStyles = { display: 'block', width, height, borderRadius, backgroundColor: animation === 'wave' ? 'transparent' : backgroundColor, ...getAnimationStyles(), ...style, }; return (_jsxs(_Fragment, { children: [_jsx("style", { children: ` @keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } @keyframes skeleton-wave { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } ` }), _jsx("div", { className: `skeleton ${className}`, style: skeletonStyles, "aria-label": "Loading...", role: "status" })] })); }; // ============================================================================ // SPECIALIZED SKELETON COMPONENTS // ============================================================================ /** * Skeleton for article/blog content with header, body, and optional elements */ export const ContentSkeleton = ({ lines = 4, showHeader = true, showImage = false, showAuthor = true, className = '', animation = 'pulse', }) => { return (_jsxs("div", { className: `content-skeleton ${className}`, style: { padding: '1.5rem' }, children: [showHeader && (_jsxs("div", { style: { marginBottom: '1.5rem' }, children: [_jsx(Skeleton, { height: "2rem", width: "85%", animation: animation, style: { marginBottom: '0.5rem' } }), _jsx(Skeleton, { height: "1.2rem", width: "60%", animation: animation })] })), showAuthor && (_jsxs("div", { style: { display: 'flex', alignItems: 'center', marginBottom: '1.5rem' }, children: [_jsx(Skeleton, { width: "2.5rem", height: "2.5rem", borderRadius: "50%", animation: animation, style: { marginRight: '0.75rem' } }), _jsxs("div", { style: { flex: 1 }, children: [_jsx(Skeleton, { height: "1rem", width: "8rem", animation: animation, style: { marginBottom: '0.25rem' } }), _jsx(Skeleton, { height: "0.875rem", width: "6rem", animation: animation })] })] })), showImage && (_jsx(Skeleton, { height: "12rem", width: "100%", borderRadius: "8px", animation: animation, style: { marginBottom: '1.5rem' } })), _jsx("div", { children: Array.from({ length: lines }, (_, index) => (_jsx(Skeleton, { height: "1rem", width: index === lines - 1 ? '70%' : '100%', animation: animation, style: { marginBottom: '0.75rem' } }, index))) })] })); }; /** * Skeleton for card components */ export const CardSkeleton = ({ count = 3, showImage = true, className = '', animation = 'pulse', }) => { return (_jsx("div", { className: `card-skeleton-container ${className}`, style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }, children: Array.from({ length: count }, (_, index) => (_jsxs("div", { style: { border: '1px solid #e0e0e0', borderRadius: '8px', overflow: 'hidden', backgroundColor: '#fff' }, children: [showImage && (_jsx(Skeleton, { height: "10rem", width: "100%", borderRadius: "0", animation: animation })), _jsxs("div", { style: { padding: '1.25rem' }, children: [_jsx(Skeleton, { height: "1.25rem", width: "90%", animation: animation, style: { marginBottom: '0.75rem' } }), _jsx(Skeleton, { height: "1rem", width: "100%", animation: animation, style: { marginBottom: '0.5rem' } }), _jsx(Skeleton, { height: "1rem", width: "85%", animation: animation, style: { marginBottom: '1rem' } }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [_jsx(Skeleton, { width: "1.5rem", height: "1.5rem", borderRadius: "50%", animation: animation }), _jsx(Skeleton, { height: "0.875rem", width: "4rem", animation: animation })] })] })] }, index))) })); }; /** * Skeleton for list components */ export const ListSkeleton = ({ count = 5, showAvatar = true, lines = 2, className = '', animation = 'pulse', }) => { return (_jsx("div", { className: `list-skeleton ${className}`, children: Array.from({ length: count }, (_, index) => (_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', padding: '1rem', borderBottom: '1px solid #f0f0f0', gap: '0.75rem' }, children: [showAvatar && (_jsx(Skeleton, { width: "3rem", height: "3rem", borderRadius: "50%", animation: animation })), _jsx("div", { style: { flex: 1 }, children: Array.from({ length: lines }, (_, lineIndex) => (_jsx(Skeleton, { height: "1rem", width: lineIndex === 0 ? '80%' : '60%', animation: animation, style: { marginBottom: lineIndex < lines - 1 ? '0.5rem' : '0' } }, lineIndex))) }), _jsx(Skeleton, { width: "2rem", height: "2rem", borderRadius: "4px", animation: animation })] }, index))) })); }; // ============================================================================ // LOADING FALLBACK COMPONENTS // ============================================================================ /** * General loading fallback component for use with Suspense */ export const LoadingFallback = ({ message = 'Loading content...', animation = 'pulse', className = '' }) => { return (_jsxs("div", { className: `loading-fallback ${className}`, style: { padding: '2rem', textAlign: 'center' }, children: [_jsx(ContentSkeleton, { lines: 3, showHeader: true, showImage: false, showAuthor: true, animation: animation }), message && (_jsx("p", { style: { marginTop: '1rem', color: '#666', fontSize: '0.875rem', fontStyle: 'italic' }, children: message }))] })); }; /** * CMS-specific loading component */ export const CmsLoadingFallback = ({ contentType = 'content', animation = 'pulse', className = '' }) => { const skeletonProps = { animation, showHeader: true, showImage: contentType === 'article' || contentType === 'blog', showAuthor: contentType === 'article' || contentType === 'blog', lines: contentType === 'page' ? 6 : 4, }; return (_jsx("div", { className: `cms-loading-fallback ${className}`, children: _jsx(ContentSkeleton, { ...skeletonProps }) })); }; /** * Button loading component */ export const ButtonLoading = ({ size = 'medium', variant = 'spinner', color = '#666' }) => { const sizeMap = { small: 16, medium: 20, large: 24, }; const spinnerSize = sizeMap[size]; if (variant === 'spinner') { return (_jsxs(_Fragment, { children: [_jsx("style", { children: ` @keyframes button-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ` }), _jsx("div", { style: { width: spinnerSize, height: spinnerSize, border: `2px solid transparent`, borderTop: `2px solid ${color}`, borderRadius: '50%', animation: 'button-spinner 1s linear infinite', }, "aria-label": "Loading...", role: "status" })] })); } if (variant === 'dots') { return (_jsxs(_Fragment, { children: [_jsx("style", { children: ` @keyframes button-dots { 0%, 20% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } } ` }), _jsx("div", { style: { display: 'flex', gap: '2px', alignItems: 'center' }, children: [0, 1, 2].map((index) => (_jsx("div", { style: { width: size === 'small' ? 3 : size === 'medium' ? 4 : 5, height: size === 'small' ? 3 : size === 'medium' ? 4 : 5, backgroundColor: color, borderRadius: '50%', animation: 'button-dots 1.4s infinite ease-in-out', animationDelay: `${index * 0.16}s`, } }, index))) })] })); } if (variant === 'pulse') { return (_jsx(Skeleton, { width: spinnerSize * 3, height: spinnerSize, animation: "pulse", backgroundColor: color, borderRadius: "4px" })); } return null; }; /** * Page loading overlay */ export const PageLoadingOverlay = ({ isVisible, message = 'Loading...', variant = 'spinner', className = '' }) => { if (!isVisible) { return null; } return (_jsx("div", { className: `page-loading-overlay ${className}`, style: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, }, children: _jsxs("div", { style: { textAlign: 'center', maxWidth: '300px' }, children: [variant === 'spinner' ? (_jsx(ButtonLoading, { size: "large", variant: "spinner", color: "#666" })) : (_jsx(ContentSkeleton, { lines: 2, showHeader: false, showAuthor: false })), message && (_jsx("p", { style: { marginTop: '1rem', color: '#666', fontSize: '1rem' }, children: message }))] }) })); }; // ============================================================================ // HOOKS FOR LOADING STATES // ============================================================================ /** * Hook for managing async operations with loading states */ export function useAsyncOperation() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); const execute = useCallback(async (asyncFunction) => { setIsLoading(true); setError(null); try { const result = await asyncFunction(); setData(result); return result; } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); setError(error); throw error; } finally { setIsLoading(false); } }, []); const reset = useCallback(() => { setIsLoading(false); setError(null); setData(null); }, []); return { isLoading, error, data, execute, reset, }; } /** * Hook for delayed loading states (prevents flash of loading state) */ export function useDelayedLoading(isLoading, delay = 200) { const [shouldShowLoading, setShouldShowLoading] = useState(false); useEffect(() => { let timeoutId; if (isLoading) { timeoutId = setTimeout(() => { setShouldShowLoading(true); }, delay); } else { setShouldShowLoading(false); } return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [isLoading, delay]); return shouldShowLoading; } export default LoadingFallback;