'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 && (
)}
>
)
}