@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
357 lines (356 loc) • 19.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getErrorMessage = getErrorMessage;
exports.FallbackContentProvider = FallbackContentProvider;
exports.useFallbackContent = useFallbackContent;
exports.FallbackContent = FallbackContent;
exports.ContentFallback = ContentFallback;
exports.PartialContentFallback = PartialContentFallback;
const jsx_runtime_1 = require("react/jsx-runtime");
/**
* @fileoverview Fallback Content Components & Context
*
* This module provides components and context for handling fallback content
* when primary content fails to load. Includes caching, error handling,
* and graceful degradation.
*/
const react_1 = require("react");
const utils_1 = require("../utils");
// ============================================================================
// CONSTANTS & UTILITIES
// ============================================================================
const STORAGE_KEY = 'ai-growth-fallback-content';
const MAX_STORAGE_SIZE = 5 * 1024 * 1024; // 5MB
const MAX_ITEMS = 100;
function getStoredFallbackContent() {
try {
if (typeof window === 'undefined' || !window.localStorage) {
return new Map();
}
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored)
return new Map();
const data = JSON.parse(stored);
const map = new Map();
Object.entries(data).forEach(([key, value]) => {
if (value && typeof value === 'object') {
map.set(key, {
...value,
cachedAt: new Date(value.cachedAt),
expiresAt: value.expiresAt ? new Date(value.expiresAt) : undefined,
});
}
});
return map;
}
catch (error) {
console.warn('Failed to load fallback content from storage:', error);
return new Map();
}
}
function storeFallbackContentToStorage(content) {
try {
if (typeof window === 'undefined' || !window.localStorage) {
return;
}
const serializable = Object.fromEntries(content);
const serialized = JSON.stringify(serializable);
// Check size limit
if (serialized.length > MAX_STORAGE_SIZE) {
console.warn('Fallback content exceeds size limit, clearing old entries');
return;
}
localStorage.setItem(STORAGE_KEY, serialized);
}
catch (error) {
console.warn('Failed to store fallback content:', error);
(0, utils_1.logError)(error, {
custom: { component: 'FallbackContent', action: 'storage' }
});
}
}
function cleanupOldEntries(content) {
const now = new Date();
const entries = Array.from(content.entries());
// Remove expired entries
const validEntries = entries.filter(([, item]) => {
return !item.expiresAt || item.expiresAt > now;
});
// If still too many, remove oldest
if (validEntries.length > MAX_ITEMS) {
validEntries.sort((a, b) => b[1].cachedAt.getTime() - a[1].cachedAt.getTime());
validEntries.splice(MAX_ITEMS);
}
content.clear();
validEntries.forEach(([key, value]) => content.set(key, value));
}
function getErrorMessage(error, _context) {
// Network errors
if (error.name === 'NetworkError' || error.message.includes('fetch')) {
return {
title: 'Connection Problem',
message: 'There was a problem connecting to our servers. Please check your internet connection and try again.',
suggestions: [
'Check your internet connection',
'Try refreshing the page',
'Wait a moment and try again'
],
showRetry: true,
showRefresh: true,
icon: 'offline'
};
}
// Authentication/Authorization errors
if (error.message.includes('Unauthorized') || error.message.includes('auth') || error.message.includes('Access denied')) {
return {
title: 'Access Issue',
message: 'You don\'t have permission to access this content.',
suggestions: [
'Try logging out and logging back in',
'Check if your session has expired',
'Contact support if you should have access'
],
showRetry: true,
showRefresh: false,
icon: 'warning'
};
}
// Not found errors (404)
if (error.message.includes('404') || error.message.includes('not found')) {
return {
title: 'Content Not Found',
message: 'The content you\'re looking for could not be found.',
suggestions: [
'Check the URL for typos',
'Go back and try a different link',
'Search for what you\'re looking for'
],
showRetry: false,
showRefresh: false,
icon: 'warning'
};
}
// Timeout errors
if (error.name === 'TimeoutError' || error.message.includes('timeout')) {
return {
title: 'Request Timed Out',
message: 'The request took too long to complete. This might be due to a slow connection or server issues.',
suggestions: [
'Try again with a better connection',
'Wait a moment before retrying',
'Check if the service is temporarily unavailable'
],
showRetry: true,
showRefresh: false,
icon: 'loading'
};
}
// Rate limiting
if (error.message.includes('429') || error.message.includes('rate limit') || error.message.includes('too many requests')) {
return {
title: 'Too Many Requests',
message: 'You\'ve made too many requests in a short time. Please wait a moment before trying again.',
suggestions: [
'Wait a few minutes before trying again',
'Reduce the frequency of your requests'
],
showRetry: false,
showRefresh: false,
icon: 'warning'
};
}
// Server errors (5xx)
if (error.message.includes('500') || error.message.includes('502') || error.message.includes('503') || error.message.includes('server error')) {
return {
title: 'Server Issue',
message: 'Our servers are experiencing temporary difficulties. Our team has been notified and is working on a fix.',
suggestions: [
'Wait a few minutes and try again',
'Check our status page for updates',
'Contact support if the problem persists'
],
showRetry: true,
showRefresh: true,
icon: 'error'
};
}
// Generic error
return {
title: 'Something Went Wrong',
message: 'We encountered an unexpected problem. This has been reported to our team and we\'re working on a fix.',
suggestions: [
'Try refreshing the page',
'Go back and try again',
'Contact support if the problem continues'
],
showRetry: true,
showRefresh: true,
icon: 'error'
};
}
// ============================================================================
// CONTEXT PROVIDER
// ============================================================================
const FallbackContentContext = (0, react_1.createContext)(null);
function FallbackContentProvider({ children, maxStorageSize: _maxStorageSize = MAX_STORAGE_SIZE, maxItems = MAX_ITEMS, defaultExpiryTime = 24 * 60 * 60 * 1000, // 24 hours
}) {
const [fallbackContent, setFallbackContent] = (0, react_1.useState)(() => getStoredFallbackContent());
// Save to localStorage whenever content changes
(0, react_1.useEffect)(() => {
storeFallbackContentToStorage(fallbackContent);
}, [fallbackContent]);
const storeFallbackContent = (0, react_1.useCallback)((item) => {
setFallbackContent(prev => {
const newContent = new Map(prev);
// Set default expiry if not provided
if (!item.expiresAt) {
item.expiresAt = new Date(Date.now() + defaultExpiryTime);
}
newContent.set(item.id, item);
// Cleanup if needed
if (newContent.size > maxItems) {
cleanupOldEntries(newContent);
}
return newContent;
});
}, [defaultExpiryTime, maxItems]);
const getFallbackContent = (0, react_1.useCallback)((id, options = {}) => {
const item = fallbackContent.get(id);
if (!item)
return null;
const now = new Date();
// Check if expired
if (item.expiresAt && item.expiresAt < now && !options.includeExpired) {
return null;
}
// Check max age
if (options.maxAge) {
const age = now.getTime() - item.cachedAt.getTime();
if (age > options.maxAge) {
return null;
}
}
// Check content type
if (options.contentType && item.type !== options.contentType) {
return null;
}
// Check priority
if (options.priority && item.priority !== options.priority) {
return null;
}
return item;
}, [fallbackContent]);
const getFallbacksByType = (0, react_1.useCallback)((type, options = {}) => {
const items = Array.from(fallbackContent.values())
.filter(item => item.type === type);
const now = new Date();
return items
.filter(item => {
// Check if expired
if (item.expiresAt && item.expiresAt < now && !options.includeExpired) {
return false;
}
// Check max age
if (options.maxAge) {
const age = now.getTime() - item.cachedAt.getTime();
if (age > options.maxAge) {
return false;
}
}
// Check priority
if (options.priority && item.priority !== options.priority) {
return false;
}
return true;
})
.sort((a, b) => {
// Sort by priority first, then by cache date
const priorityOrder = { high: 3, medium: 2, low: 1 };
const aPriority = priorityOrder[a.priority || 'medium'];
const bPriority = priorityOrder[b.priority || 'medium'];
if (aPriority !== bPriority) {
return bPriority - aPriority;
}
return b.cachedAt.getTime() - a.cachedAt.getTime();
});
}, [fallbackContent]);
const clearFallbackContent = (0, react_1.useCallback)((id) => {
setFallbackContent(prev => {
if (id) {
const newContent = new Map(prev);
newContent.delete(id);
return newContent;
}
return new Map();
});
}, []);
const hasFallbackContent = (0, react_1.useCallback)((id) => {
return fallbackContent.has(id);
}, [fallbackContent]);
const getStorageInfo = (0, react_1.useCallback)(() => {
const serialized = JSON.stringify(Object.fromEntries(fallbackContent));
return {
count: fallbackContent.size,
size: new Blob([serialized]).size,
};
}, [fallbackContent]);
const value = {
storeFallbackContent,
getFallbackContent,
getFallbacksByType,
clearFallbackContent,
hasFallbackContent,
getStorageInfo,
};
return ((0, jsx_runtime_1.jsx)(FallbackContentContext.Provider, { value: value, children: children }));
}
// ============================================================================
// HOOKS
// ============================================================================
function useFallbackContent() {
const context = (0, react_1.useContext)(FallbackContentContext);
if (!context) {
throw new Error('useFallbackContent must be used within a FallbackContentProvider');
}
return context;
}
function FallbackContent({ error, contentId, contentType, fallbackData, onRetry, onRefresh, customErrorMessage, className = '', }) {
const { getFallbackContent, getFallbacksByType } = useFallbackContent();
// Try to get fallback content
let fallbackContent = null;
if (contentId) {
fallbackContent = getFallbackContent(contentId);
}
else if (contentType) {
const fallbacks = getFallbacksByType(contentType);
fallbackContent = fallbacks[0] || null;
}
// Get error message
const errorMessage = customErrorMessage || (error ? getErrorMessage(error) : {
title: 'Content Unavailable',
message: 'The content you\'re looking for is currently unavailable.',
suggestions: ['Try refreshing the page', 'Check back later'],
showRetry: true,
showRefresh: true,
icon: 'warning',
});
// If we have fallback content, display it
if (fallbackContent || fallbackData) {
const data = fallbackData || fallbackContent?.data;
return ((0, jsx_runtime_1.jsxs)("div", { className: `fallback-content ${className}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "fallback-content__header", children: [(0, jsx_runtime_1.jsxs)("div", { className: "fallback-content__notice", children: [(0, jsx_runtime_1.jsx)("span", { className: "fallback-content__icon", children: "\u26A0\uFE0F" }), (0, jsx_runtime_1.jsx)("span", { children: "Showing cached content" })] }), (onRetry || onRefresh) && ((0, jsx_runtime_1.jsxs)("div", { className: "fallback-content__actions", children: [onRetry && ((0, jsx_runtime_1.jsx)("button", { onClick: onRetry, className: "fallback-content__button fallback-content__button--retry", children: "Try Again" })), onRefresh && ((0, jsx_runtime_1.jsx)("button", { onClick: onRefresh, className: "fallback-content__button fallback-content__button--refresh", children: "Refresh" }))] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "fallback-content__body", children: typeof data === 'string' ? ((0, jsx_runtime_1.jsx)("div", { dangerouslySetInnerHTML: { __html: data } })) : fallbackContent ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: fallbackContent.title }), fallbackContent.description && (0, jsx_runtime_1.jsx)("p", { children: fallbackContent.description }), typeof data?.content === 'string' && ((0, jsx_runtime_1.jsx)("div", { dangerouslySetInnerHTML: { __html: data.content } }))] })) : ((0, jsx_runtime_1.jsx)("div", { children: JSON.stringify(data, null, 2) })) }), fallbackContent && ((0, jsx_runtime_1.jsx)("div", { className: "fallback-content__meta", children: (0, jsx_runtime_1.jsxs)("small", { children: ["Last updated: ", fallbackContent.cachedAt.toLocaleDateString(), " at", ' ', fallbackContent.cachedAt.toLocaleTimeString()] }) }))] }));
}
// Display error message
return ((0, jsx_runtime_1.jsx)("div", { className: `error-message ${className}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "error-message__content", children: [(0, jsx_runtime_1.jsxs)("div", { className: "error-message__icon", children: [errorMessage.icon === 'warning' && '⚠️', errorMessage.icon === 'error' && '❌', errorMessage.icon === 'offline' && '📡', errorMessage.icon === 'loading' && '⏳'] }), (0, jsx_runtime_1.jsx)("h3", { className: "error-message__title", children: errorMessage.title }), (0, jsx_runtime_1.jsx)("p", { className: "error-message__description", children: errorMessage.message }), errorMessage.suggestions && errorMessage.suggestions.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "error-message__suggestions", children: [(0, jsx_runtime_1.jsx)("h4", { children: "What you can try:" }), (0, jsx_runtime_1.jsx)("ul", { children: errorMessage.suggestions.map((suggestion, index) => ((0, jsx_runtime_1.jsx)("li", { children: suggestion }, index))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "error-message__actions", children: [errorMessage.showRetry && onRetry && ((0, jsx_runtime_1.jsx)("button", { onClick: onRetry, className: "error-message__button error-message__button--primary", children: "Try Again" })), errorMessage.showRefresh && onRefresh && ((0, jsx_runtime_1.jsx)("button", { onClick: onRefresh, className: "error-message__button error-message__button--secondary", children: "Refresh Page" }))] })] }) }));
}
function ContentFallback({ contentType, count = 3, error, onRetry, className = '', }) {
const { getFallbacksByType } = useFallbackContent();
const fallbacks = getFallbacksByType(contentType).slice(0, count);
if (fallbacks.length === 0) {
return ((0, jsx_runtime_1.jsx)(FallbackContent, { ...(error && { error }), contentType: contentType, ...(onRetry && { onRetry }), className: className }));
}
return ((0, jsx_runtime_1.jsxs)("div", { className: `content-fallback ${className}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "content-fallback__header", children: [(0, jsx_runtime_1.jsxs)("div", { className: "content-fallback__notice", children: [(0, jsx_runtime_1.jsx)("span", { className: "content-fallback__icon", children: "\u26A0\uFE0F" }), (0, jsx_runtime_1.jsxs)("span", { children: ["Showing cached ", contentType, " content"] })] }), onRetry && ((0, jsx_runtime_1.jsx)("button", { onClick: onRetry, className: "content-fallback__retry", children: "Try Again" }))] }), (0, jsx_runtime_1.jsx)("div", { className: "content-fallback__grid", children: fallbacks.map((fallback) => ((0, jsx_runtime_1.jsxs)("div", { className: "content-fallback__item", children: [(0, jsx_runtime_1.jsx)("h4", { className: "content-fallback__title", children: fallback.title }), fallback.description && ((0, jsx_runtime_1.jsx)("p", { className: "content-fallback__description", children: fallback.description })), (0, jsx_runtime_1.jsx)("div", { className: "content-fallback__meta", children: (0, jsx_runtime_1.jsxs)("small", { children: ["Cached: ", fallback.cachedAt.toLocaleDateString()] }) })] }, fallback.id))) })] }));
}
function PartialContentFallback({ partialData, error: _error, onRetry, missingFields = [], className = '', }) {
return ((0, jsx_runtime_1.jsxs)("div", { className: `partial-content-fallback ${className}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "partial-content-fallback__notice", children: [(0, jsx_runtime_1.jsx)("span", { className: "partial-content-fallback__icon", children: "\u26A0\uFE0F" }), (0, jsx_runtime_1.jsx)("span", { children: "Some content couldn't be loaded" }), onRetry && ((0, jsx_runtime_1.jsx)("button", { onClick: onRetry, className: "partial-content-fallback__retry", children: "Try Again" }))] }), (0, jsx_runtime_1.jsx)("div", { className: "partial-content-fallback__content", children: typeof partialData === 'object' ? (Object.entries(partialData).map(([key, value]) => ((0, jsx_runtime_1.jsxs)("div", { className: "partial-content-fallback__field", children: [(0, jsx_runtime_1.jsxs)("strong", { children: [key, ":"] }), " ", String(value)] }, key)))) : ((0, jsx_runtime_1.jsx)("div", { children: String(partialData) })) }), missingFields.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "partial-content-fallback__missing", children: (0, jsx_runtime_1.jsxs)("small", { children: ["Missing: ", missingFields.join(', ')] }) }))] }));
}
exports.default = FallbackContent;