UNPKG

@ai-growth/nextjs

Version:

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

320 lines (318 loc) 16.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PageLoadingOverlay = exports.ButtonLoading = exports.CmsLoadingFallback = exports.LoadingFallback = exports.ListSkeleton = exports.CardSkeleton = exports.ContentSkeleton = exports.Skeleton = exports.useOperationLoading = exports.useLoading = exports.LoadingProvider = void 0; exports.useAsyncOperation = useAsyncOperation; exports.useDelayedLoading = useDelayedLoading; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); // ============================================================================ // LOADING CONTEXT // ============================================================================ const LoadingContext = (0, react_1.createContext)(null); /** * Provider for managing global and operation-specific loading states */ const LoadingProvider = ({ children, initialLoading = false, }) => { const [isLoading, setIsLoading] = (0, react_1.useState)(initialLoading); const [loadingStates, setLoadingStates] = (0, react_1.useState)({}); const setLoading = (0, react_1.useCallback)((loading) => { setIsLoading(loading); }, []); const setOperationLoading = (0, react_1.useCallback)((operation, loading) => { setLoadingStates(prev => ({ ...prev, [operation]: loading, })); }, []); const isOperationLoading = (0, react_1.useCallback)((operation) => { return loadingStates[operation] || false; }, [loadingStates]); const contextValue = { isLoading, setLoading, loadingStates, setOperationLoading, isOperationLoading, }; return ((0, jsx_runtime_1.jsx)(LoadingContext.Provider, { value: contextValue, children: children })); }; exports.LoadingProvider = LoadingProvider; /** * Hook to access loading context */ const useLoading = () => { const context = (0, react_1.useContext)(LoadingContext); if (!context) { throw new Error('useLoading must be used within a LoadingProvider'); } return context; }; exports.useLoading = useLoading; /** * Hook for managing operation-specific loading states */ const useOperationLoading = (operationName) => { const { isOperationLoading, setOperationLoading } = (0, exports.useLoading)(); const isLoading = isOperationLoading(operationName); const setLoading = (0, react_1.useCallback)((loading) => { setOperationLoading(operationName, loading); }, [operationName, setOperationLoading]); return { isLoading, setLoading }; }; exports.useOperationLoading = useOperationLoading; // ============================================================================ // BASE SKELETON COMPONENT // ============================================================================ /** * Base skeleton component with customizable appearance and animations */ 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 ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.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; } } ` }), (0, jsx_runtime_1.jsx)("div", { className: `skeleton ${className}`, style: skeletonStyles, "aria-label": "Loading...", role: "status" })] })); }; exports.Skeleton = Skeleton; // ============================================================================ // SPECIALIZED SKELETON COMPONENTS // ============================================================================ /** * Skeleton for article/blog content with header, body, and optional elements */ const ContentSkeleton = ({ lines = 4, showHeader = true, showImage = false, showAuthor = true, className = '', animation = 'pulse', }) => { return ((0, jsx_runtime_1.jsxs)("div", { className: `content-skeleton ${className}`, style: { padding: '1.5rem' }, children: [showHeader && ((0, jsx_runtime_1.jsxs)("div", { style: { marginBottom: '1.5rem' }, children: [(0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "2rem", width: "85%", animation: animation, style: { marginBottom: '0.5rem' } }), (0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1.2rem", width: "60%", animation: animation })] })), showAuthor && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', marginBottom: '1.5rem' }, children: [(0, jsx_runtime_1.jsx)(exports.Skeleton, { width: "2.5rem", height: "2.5rem", borderRadius: "50%", animation: animation, style: { marginRight: '0.75rem' } }), (0, jsx_runtime_1.jsxs)("div", { style: { flex: 1 }, children: [(0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1rem", width: "8rem", animation: animation, style: { marginBottom: '0.25rem' } }), (0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "0.875rem", width: "6rem", animation: animation })] })] })), showImage && ((0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "12rem", width: "100%", borderRadius: "8px", animation: animation, style: { marginBottom: '1.5rem' } })), (0, jsx_runtime_1.jsx)("div", { children: Array.from({ length: lines }, (_, index) => ((0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1rem", width: index === lines - 1 ? '70%' : '100%', animation: animation, style: { marginBottom: '0.75rem' } }, index))) })] })); }; exports.ContentSkeleton = ContentSkeleton; /** * Skeleton for card components */ const CardSkeleton = ({ count = 3, showImage = true, className = '', animation = 'pulse', }) => { return ((0, jsx_runtime_1.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) => ((0, jsx_runtime_1.jsxs)("div", { style: { border: '1px solid #e0e0e0', borderRadius: '8px', overflow: 'hidden', backgroundColor: '#fff' }, children: [showImage && ((0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "10rem", width: "100%", borderRadius: "0", animation: animation })), (0, jsx_runtime_1.jsxs)("div", { style: { padding: '1.25rem' }, children: [(0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1.25rem", width: "90%", animation: animation, style: { marginBottom: '0.75rem' } }), (0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1rem", width: "100%", animation: animation, style: { marginBottom: '0.5rem' } }), (0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1rem", width: "85%", animation: animation, style: { marginBottom: '1rem' } }), (0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: [(0, jsx_runtime_1.jsx)(exports.Skeleton, { width: "1.5rem", height: "1.5rem", borderRadius: "50%", animation: animation }), (0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "0.875rem", width: "4rem", animation: animation })] })] })] }, index))) })); }; exports.CardSkeleton = CardSkeleton; /** * Skeleton for list components */ const ListSkeleton = ({ count = 5, showAvatar = true, lines = 2, className = '', animation = 'pulse', }) => { return ((0, jsx_runtime_1.jsx)("div", { className: `list-skeleton ${className}`, children: Array.from({ length: count }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', alignItems: 'flex-start', padding: '1rem', borderBottom: '1px solid #f0f0f0', gap: '0.75rem' }, children: [showAvatar && ((0, jsx_runtime_1.jsx)(exports.Skeleton, { width: "3rem", height: "3rem", borderRadius: "50%", animation: animation })), (0, jsx_runtime_1.jsx)("div", { style: { flex: 1 }, children: Array.from({ length: lines }, (_, lineIndex) => ((0, jsx_runtime_1.jsx)(exports.Skeleton, { height: "1rem", width: lineIndex === 0 ? '80%' : '60%', animation: animation, style: { marginBottom: lineIndex < lines - 1 ? '0.5rem' : '0' } }, lineIndex))) }), (0, jsx_runtime_1.jsx)(exports.Skeleton, { width: "2rem", height: "2rem", borderRadius: "4px", animation: animation })] }, index))) })); }; exports.ListSkeleton = ListSkeleton; // ============================================================================ // LOADING FALLBACK COMPONENTS // ============================================================================ /** * General loading fallback component for use with Suspense */ const LoadingFallback = ({ message = 'Loading content...', animation = 'pulse', className = '' }) => { return ((0, jsx_runtime_1.jsxs)("div", { className: `loading-fallback ${className}`, style: { padding: '2rem', textAlign: 'center' }, children: [(0, jsx_runtime_1.jsx)(exports.ContentSkeleton, { lines: 3, showHeader: true, showImage: false, showAuthor: true, animation: animation }), message && ((0, jsx_runtime_1.jsx)("p", { style: { marginTop: '1rem', color: '#666', fontSize: '0.875rem', fontStyle: 'italic' }, children: message }))] })); }; exports.LoadingFallback = LoadingFallback; /** * CMS-specific loading component */ 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 ((0, jsx_runtime_1.jsx)("div", { className: `cms-loading-fallback ${className}`, children: (0, jsx_runtime_1.jsx)(exports.ContentSkeleton, { ...skeletonProps }) })); }; exports.CmsLoadingFallback = CmsLoadingFallback; /** * Button loading component */ const ButtonLoading = ({ size = 'medium', variant = 'spinner', color = '#666' }) => { const sizeMap = { small: 16, medium: 20, large: 24, }; const spinnerSize = sizeMap[size]; if (variant === 'spinner') { return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { children: ` @keyframes button-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ` }), (0, jsx_runtime_1.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 ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { children: ` @keyframes button-dots { 0%, 20% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } } ` }), (0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', gap: '2px', alignItems: 'center' }, children: [0, 1, 2].map((index) => ((0, jsx_runtime_1.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 ((0, jsx_runtime_1.jsx)(exports.Skeleton, { width: spinnerSize * 3, height: spinnerSize, animation: "pulse", backgroundColor: color, borderRadius: "4px" })); } return null; }; exports.ButtonLoading = ButtonLoading; /** * Page loading overlay */ const PageLoadingOverlay = ({ isVisible, message = 'Loading...', variant = 'spinner', className = '' }) => { if (!isVisible) { return null; } return ((0, jsx_runtime_1.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: (0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', maxWidth: '300px' }, children: [variant === 'spinner' ? ((0, jsx_runtime_1.jsx)(exports.ButtonLoading, { size: "large", variant: "spinner", color: "#666" })) : ((0, jsx_runtime_1.jsx)(exports.ContentSkeleton, { lines: 2, showHeader: false, showAuthor: false })), message && ((0, jsx_runtime_1.jsx)("p", { style: { marginTop: '1rem', color: '#666', fontSize: '1rem' }, children: message }))] }) })); }; exports.PageLoadingOverlay = PageLoadingOverlay; // ============================================================================ // HOOKS FOR LOADING STATES // ============================================================================ /** * Hook for managing async operations with loading states */ function useAsyncOperation() { const [isLoading, setIsLoading] = (0, react_1.useState)(false); const [error, setError] = (0, react_1.useState)(null); const [data, setData] = (0, react_1.useState)(null); const execute = (0, react_1.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 = (0, react_1.useCallback)(() => { setIsLoading(false); setError(null); setData(null); }, []); return { isLoading, error, data, execute, reset, }; } /** * Hook for delayed loading states (prevents flash of loading state) */ function useDelayedLoading(isLoading, delay = 200) { const [shouldShowLoading, setShouldShowLoading] = (0, react_1.useState)(false); (0, react_1.useEffect)(() => { let timeoutId; if (isLoading) { timeoutId = setTimeout(() => { setShouldShowLoading(true); }, delay); } else { setShouldShowLoading(false); } return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [isLoading, delay]); return shouldShowLoading; } exports.default = exports.LoadingFallback;