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