UNPKG

@ai-growth/nextjs

Version:

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

193 lines (192 loc) 6.37 kB
import { useEffect, useState, useCallback, useMemo } from 'react'; import { getDocumentsByType } from '../utils'; const listCache = new Map(); /** * React hook for fetching lists of CMS content with pagination */ export function useCmsContentList(options) { const { contentType, enabled = true, staleTime = 5 * 60 * 1000, // 5 minutes onSuccess, onError, limit = 10, offset = 0, ...fetchOptions } = options; const [content, setContent] = useState([]); const [isLoading, setIsLoading] = useState(enabled); const [isError, setIsError] = useState(false); const [error, setError] = useState(null); const [isFetching, setIsFetching] = useState(false); const [hasMore, setHasMore] = useState(true); const [totalCount, setTotalCount] = useState(); const [currentOffset, setCurrentOffset] = useState(offset); // Generate cache key const cacheKey = useMemo(() => { return `list:${contentType}:${JSON.stringify({ ...fetchOptions, limit, offset: 0, // Always cache from beginning })}`; }, [contentType, fetchOptions, limit]); // Check if cached data is stale const isStale = useMemo(() => { const cached = listCache.get(cacheKey); if (!cached) return true; return Date.now() - cached.timestamp > cached.staleTime; }, [cacheKey, content]); // Transform Sanity documents to CmsContent format const transformDocuments = useCallback((documents) => { return documents.map(doc => ({ _id: doc._id, _type: doc._type, slug: doc.slug?.current || '', title: doc.title || 'Untitled', content: doc.content || doc.body || null, metadata: doc.seo, publishedAt: doc.publishedAt, author: doc.author, })); }, []); // Fetch content function const fetchContent = useCallback(async (isLoadMore = false) => { if (!enabled) return; const effectiveOffset = isLoadMore ? currentOffset : 0; // Check cache for initial load if (!isLoadMore && !isStale && !isFetching) { const cached = listCache.get(cacheKey); if (cached) { setContent(cached.data); setHasMore(cached.hasMore); setTotalCount(cached.totalCount); setIsLoading(false); setIsError(false); setError(null); return; } } try { setIsFetching(true); setIsError(false); setError(null); if (!isLoadMore) { setIsLoading(true); } const result = await getDocumentsByType(contentType, { ...fetchOptions, limit, offset: effectiveOffset, includeTotal: true, }); const transformedContent = transformDocuments(result.documents); const newHasMore = result.documents.length === limit; const newTotalCount = result.total; if (isLoadMore) { // Append to existing content setContent(prev => [...prev, ...transformedContent]); setCurrentOffset(prev => prev + limit); } else { // Replace content setContent(transformedContent); setCurrentOffset(limit); // Cache the initial result listCache.set(cacheKey, { data: transformedContent, timestamp: Date.now(), staleTime, hasMore: newHasMore, totalCount: newTotalCount ?? 0, }); } setHasMore(newHasMore); setTotalCount(newTotalCount); setIsLoading(false); onSuccess?.(isLoadMore ? content.concat(transformedContent) : transformedContent); } catch (err) { const errorObj = err instanceof Error ? err : new Error(String(err)); setError(errorObj); setIsError(true); setIsLoading(false); onError?.(errorObj); } finally { setIsFetching(false); } }, [ enabled, contentType, fetchOptions, limit, currentOffset, cacheKey, isStale, isFetching, staleTime, transformDocuments, onSuccess, onError, content, ]); // Load more function const loadMore = useCallback(async () => { if (!hasMore || isFetching) return; await fetchContent(true); }, [hasMore, isFetching, fetchContent]); // Refetch function for manual refresh const refetch = useCallback(async () => { listCache.delete(cacheKey); // Clear cache setCurrentOffset(0); await fetchContent(false); }, [cacheKey, fetchContent]); // Reset function to clear state const reset = useCallback(() => { setContent([]); setIsLoading(enabled); setIsError(false); setError(null); setIsFetching(false); setHasMore(true); setTotalCount(undefined); setCurrentOffset(0); }, [enabled]); // Initial fetch effect useEffect(() => { if (enabled) { fetchContent(false); } }, [enabled, contentType, JSON.stringify(fetchOptions)]); return { content, isLoading, isError, error, isFetching, hasMore, totalCount: totalCount ?? 0, refetch, loadMore, reset, }; } /** * Hook for fetching featured/recent content */ export function useFeaturedContent(contentType = 'post', limit = 5, options = {}) { return useCmsContentList({ ...options, contentType, limit, orderBy: 'publishedAt desc', additionalFilter: options.additionalFilter || 'featured == true', }); } /** * Hook for fetching recent content */ export function useRecentContent(contentType = 'post', limit = 10, options = {}) { return useCmsContentList({ ...options, contentType, limit, orderBy: 'publishedAt desc', }); }