'use client' import { type BlogView } from 'app/blog/blog-view' 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 SectionContainerWithCn from '../../components/Layouts/SectionContainerWithCn' import SectionContainer from '@/components/Layouts/SectionContainer' const POSTS_PER_PAGE = 25 const SKELETON_COUNT = 6 // Featured + 2 secondary posts shown by the layout hero on the index. Excluded // from the list to avoid duplication — keep in sync with app/blog/layout.tsx. const HERO_POST_COUNT = 3 function BlogListItemSkeleton() { return (
) } function BlogGridItemSkeleton() { return (
) } interface BlogClientProps { initialBlogs: any[] totalPosts: number initialView: BlogView } export default function BlogClient({ initialBlogs, totalPosts, initialView }: BlogClientProps) { const [view, setView] = useState(initialView) 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) => { // The hero posts stay in the loaded array (just rendered in the layout // hero, sliced off at render), so `offset` already counts them — no skip. const params = new URLSearchParams({ offset: offset.toString(), limit: limit.toString(), }) 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, hasMore, 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) } }, []) // The hero posts (the most recent HERO_POST_COUNT) are rendered by the blog // layout's hero, so drop them from the list to avoid duplication. Filtered // results are a separate query with no hero, so show all. const visibleBlogs = filteredPosts !== null ? blogs : blogs.slice(HERO_POST_COUNT) return (
{/* Filters row */}
{/* Blog posts */} {isFiltering ? ( isList ? (
{Array.from({ length: SKELETON_COUNT }).map((_, idx) => ( ))}
) : (
{Array.from({ length: SKELETON_COUNT }).map((_, idx) => (
))}
) ) : visibleBlogs?.length ? ( isList ? (
    {visibleBlogs.map((blog: PostTypes, idx: number) => (
  • ))}
) : (
    {visibleBlogs.map((blog: PostTypes, idx: number) => (
  • ))}
) ) : (

No results

)} {hasMore && !isFiltering && ( ) }