'use client' import { isBrowser, LOCAL_STORAGE_KEYS } from 'common' import BlogFilters from 'components/Blog/BlogFilters' import BlogGridItem from 'components/Blog/BlogGridItem' import BlogListItem from 'components/Blog/BlogListItem' import { useInfiniteScrollWithFetch } from 'hooks/useInfiniteScroll' import { Suspense, useCallback, useState } from 'react' import type PostTypes from 'types/post' import { cn } from 'ui' export type BlogView = 'list' | 'grid' const POSTS_PER_PAGE = 25 const SKELETON_COUNT = 6 function BlogListItemSkeleton() { return (
) } function BlogGridItemSkeleton() { return (
) } interface BlogClientProps { initialBlogs: any[] totalPosts: number } export default function BlogClient({ initialBlogs, totalPosts }: BlogClientProps) { const { BLOG_VIEW } = LOCAL_STORAGE_KEYS const localView = isBrowser ? (localStorage?.getItem(BLOG_VIEW) as BlogView) : undefined const [view, setView] = useState(localView ?? 'list') const [isFiltering, setIsFiltering] = useState(false) const [filterParams, setFilterParams] = useState<{ category?: string; search?: string }>({}) const [filteredPosts, setFilteredPosts] = useState(null) const [filteredTotal, setFilteredTotal] = useState(null) const isList = view === 'list' const currentPosts = filteredPosts ?? initialBlogs const currentTotal = filteredTotal ?? totalPosts const fetchMorePosts = useCallback( async (offset: number, limit: number) => { const isFiltered = (filterParams.category && filterParams.category !== 'all') || Boolean(filterParams.search) // The featured post is rendered above the list (not in `items`), so the // API offset has to skip past it for unfiltered fetches. Filtered // results come from a separate query and don't share that hero slot. const apiOffset = isFiltered ? offset : offset + 1 const params = new URLSearchParams({ offset: apiOffset.toString(), limit: limit.toString(), }) if (filterParams.category && filterParams.category !== 'all') { params.set('category', filterParams.category) } if (filterParams.search) { params.set('q', filterParams.search) } const response = await fetch(`/api-v2/blog-posts?${params}`) const data = await response.json() if (!data.success) { throw new Error(data.error || 'Failed to fetch posts') } return data.posts }, [filterParams] ) const { items: blogs, setItems: setBlogs, hasMore, isLoading, loadMoreRef, } = useInfiniteScrollWithFetch({ initialItems: currentPosts, totalItems: currentTotal, pageSize: POSTS_PER_PAGE, fetchMore: fetchMorePosts, rootMargin: '1000px', }) const handleFilterChange = useCallback(async (category?: string, search?: string) => { if ((!category || category === 'all') && !search) { setFilterParams({}) setFilteredPosts(null) setFilteredTotal(null) setIsFiltering(false) return } setIsFiltering(true) setFilterParams({ category, search }) try { const params = new URLSearchParams({ offset: '0', limit: POSTS_PER_PAGE.toString(), }) if (category && category !== 'all') { params.set('category', category) } if (search) { params.set('q', search) } const response = await fetch(`/api-v2/blog-posts?${params}`) const data = await response.json() if (data.success) { setFilteredPosts(data.posts) setFilteredTotal(data.total) } } catch (error) { console.error('Failed to fetch filtered posts:', error) } finally { setIsFiltering(false) } }, []) return ( <>
    {isFiltering ? ( isList ? ( Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
    )) ) : ( Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
    )) ) ) : blogs?.length ? ( blogs?.map((blog: PostTypes, idx: number) => isList ? (
    ) : (
    ) ) ) : (

    No results

    )}
{hasMore && !isFiltering && (