1- import { Suspense } from 'react' ;
1+ 'use client' ;
2+
3+ import { Suspense , useState , useEffect } from 'react' ;
24import { Button } from '@/components/ui/button' ;
35import { Card , CardContent , CardHeader } from '@/components/ui/card' ;
46import { Badge } from '@/components/ui/badge' ;
@@ -15,12 +17,7 @@ import {
1517} from 'lucide-react' ;
1618import Link from 'next/link' ;
1719import Image from 'next/image' ;
18- import {
19- getSortedPostsData ,
20- getFeaturedPosts ,
21- getAllCategories ,
22- BlogPost ,
23- } from '@/lib/utils/blog' ;
20+ import { BlogPost } from '@/lib/utils/blog' ;
2421
2522// Static navbar component for blog page to avoid dynamic server usage
2623const StaticBlogNavbar = ( ) => {
@@ -61,11 +58,150 @@ const StaticBlogNavbar = () => {
6158 ) ;
6259} ;
6360
64- const BlogPage = async ( ) => {
65- const featuredPosts = await getFeaturedPosts ( ) ;
66- const allPosts = await getSortedPostsData ( ) ;
67- const recentPosts = allPosts . filter ( ( post ) => ! post . featured ) . slice ( 0 , 6 ) ;
68- const categories = await getAllCategories ( ) ;
61+ const BlogPage = ( ) => {
62+ const [ featuredPosts , setFeaturedPosts ] = useState < BlogPost [ ] > ( [ ] ) ;
63+ const [ allPosts , setAllPosts ] = useState < BlogPost [ ] > ( [ ] ) ;
64+ const [ displayedPosts , setDisplayedPosts ] = useState < BlogPost [ ] > ( [ ] ) ;
65+ const [ categories , setCategories ] = useState < string [ ] > ( [ ] ) ;
66+ const [ selectedCategory , setSelectedCategory ] = useState < string > ( 'All' ) ;
67+ const [ searchTerm , setSearchTerm ] = useState < string > ( '' ) ;
68+ const [ postsPerPage ] = useState ( 6 ) ;
69+ const [ currentPostsShown , setCurrentPostsShown ] = useState ( 6 ) ;
70+ const [ loading , setLoading ] = useState ( true ) ;
71+
72+ useEffect ( ( ) => {
73+ const loadBlogData = async ( ) => {
74+ try {
75+ const [ featuredResponse , postsResponse , categoriesResponse ] =
76+ await Promise . all ( [
77+ fetch ( '/api/blog?type=featured' ) ,
78+ fetch ( '/api/blog?type=all' ) ,
79+ fetch ( '/api/blog?type=categories' ) ,
80+ ] ) ;
81+
82+ const [ featuredData , postsData , categoriesData ] = await Promise . all ( [
83+ featuredResponse . json ( ) ,
84+ postsResponse . json ( ) ,
85+ categoriesResponse . json ( ) ,
86+ ] ) ;
87+
88+ setFeaturedPosts ( featuredData . data ) ;
89+ setAllPosts ( postsData . data ) ;
90+ setCategories ( categoriesData . data ) ;
91+
92+ // Filter out featured posts for the main grid and show initial 6
93+ const nonFeaturedPosts = postsData . data . filter (
94+ ( post : BlogPost ) => ! post . featured
95+ ) ;
96+ setDisplayedPosts ( nonFeaturedPosts . slice ( 0 , postsPerPage ) ) ;
97+ setCurrentPostsShown ( Math . min ( postsPerPage , nonFeaturedPosts . length ) ) ;
98+ } catch ( error ) {
99+ console . error ( 'Error loading blog data:' , error ) ;
100+ } finally {
101+ setLoading ( false ) ;
102+ }
103+ } ;
104+
105+ loadBlogData ( ) ;
106+ } , [ postsPerPage ] ) ;
107+
108+ // Filter posts based on category and search term
109+ useEffect ( ( ) => {
110+ let filteredPosts = allPosts . filter ( ( post ) => ! post . featured ) ; // Exclude featured posts from main grid
111+
112+ if ( selectedCategory !== 'All' ) {
113+ filteredPosts = filteredPosts . filter (
114+ ( post ) => post . category === selectedCategory
115+ ) ;
116+ }
117+
118+ if ( searchTerm ) {
119+ filteredPosts = filteredPosts . filter (
120+ ( post ) =>
121+ post . title . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
122+ post . excerpt . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
123+ post . tags . some ( ( tag ) =>
124+ tag . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
125+ )
126+ ) ;
127+ }
128+
129+ setDisplayedPosts ( filteredPosts . slice ( 0 , currentPostsShown ) ) ;
130+ } , [ selectedCategory , searchTerm , allPosts , currentPostsShown ] ) ;
131+
132+ const handleLoadMore = ( ) => {
133+ const nonFeaturedPosts = allPosts . filter ( ( post ) => ! post . featured ) ;
134+ let filteredPosts = nonFeaturedPosts ;
135+
136+ if ( selectedCategory !== 'All' ) {
137+ filteredPosts = nonFeaturedPosts . filter (
138+ ( post ) => post . category === selectedCategory
139+ ) ;
140+ }
141+
142+ if ( searchTerm ) {
143+ filteredPosts = filteredPosts . filter (
144+ ( post ) =>
145+ post . title . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
146+ post . excerpt . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
147+ post . tags . some ( ( tag ) =>
148+ tag . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
149+ )
150+ ) ;
151+ }
152+
153+ const newPostsShown = Math . min (
154+ currentPostsShown + postsPerPage ,
155+ filteredPosts . length
156+ ) ;
157+ setCurrentPostsShown ( newPostsShown ) ;
158+ setDisplayedPosts ( filteredPosts . slice ( 0 , newPostsShown ) ) ;
159+ } ;
160+
161+ const handleCategoryChange = ( category : string ) => {
162+ setSelectedCategory ( category ) ;
163+ setCurrentPostsShown ( postsPerPage ) ; // Reset to initial number
164+ } ;
165+
166+ const handleSearch = ( e : React . ChangeEvent < HTMLInputElement > ) => {
167+ setSearchTerm ( e . target . value ) ;
168+ setCurrentPostsShown ( postsPerPage ) ; // Reset to initial number
169+ } ;
170+
171+ // Calculate if there are more posts to show
172+ const hasMorePosts = ( ) => {
173+ let filteredPosts = allPosts . filter ( ( post ) => ! post . featured ) ;
174+
175+ if ( selectedCategory !== 'All' ) {
176+ filteredPosts = filteredPosts . filter (
177+ ( post ) => post . category === selectedCategory
178+ ) ;
179+ }
180+
181+ if ( searchTerm ) {
182+ filteredPosts = filteredPosts . filter (
183+ ( post ) =>
184+ post . title . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
185+ post . excerpt . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
186+ post . tags . some ( ( tag ) =>
187+ tag . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) )
188+ )
189+ ) ;
190+ }
191+
192+ return currentPostsShown < filteredPosts . length ;
193+ } ;
194+
195+ if ( loading ) {
196+ return (
197+ < div className = "min-h-screen bg-white flex items-center justify-center" >
198+ < div className = "text-center" >
199+ < div className = "animate-spin rounded-full h-12 w-12 border-b-2 border-slate-600 mx-auto mb-4" > </ div >
200+ < p className = "text-slate-600" > Loading blog posts...</ p >
201+ </ div >
202+ </ div >
203+ ) ;
204+ }
69205
70206 return (
71207 < div className = "min-h-screen bg-white" >
@@ -103,6 +239,8 @@ const BlogPage = async () => {
103239 < input
104240 type = "text"
105241 placeholder = "Search articles..."
242+ value = { searchTerm }
243+ onChange = { handleSearch }
106244 className = "w-full bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-slate-400 rounded-xl px-12 py-4 focus:outline-none focus:ring-2 focus:ring-white/30 transition-all duration-300"
107245 />
108246 </ div >
@@ -113,34 +251,38 @@ const BlogPage = async () => {
113251
114252 < div className = "container mx-auto px-4 py-20" >
115253 { /* Featured Posts Section */ }
116- < section className = "mb-24" >
117- < div className = "flex items-center justify-between mb-12" >
118- < div >
119- < div className = "inline-flex items-center gap-2 text-slate-600 font-semibold text-sm mb-3" >
120- < TrendingUp className = "w-4 h-4" />
121- Featured Articles
254+ { featuredPosts . length > 0 && (
255+ < section className = "mb-24" >
256+ < div className = "flex items-center justify-between mb-12" >
257+ < div >
258+ < div className = "inline-flex items-center gap-2 text-slate-600 font-semibold text-sm mb-3" >
259+ < TrendingUp className = "w-4 h-4" />
260+ Featured Articles
261+ </ div >
262+ < h2 className = "text-4xl font-bold text-gray-900 mb-3" >
263+ Trending Stories
264+ </ h2 >
265+ < p className = "text-lg text-gray-600 max-w-2xl" >
266+ Our most popular and insightful articles about live streaming,
267+ Web3, and community building
268+ </ p >
122269 </ div >
123- < h2 className = "text-4xl font-bold text-gray-900 mb-3" >
124- Trending Stories
125- </ h2 >
126- < p className = "text-lg text-gray-600 max-w-2xl" >
127- Our most popular and insightful articles about live streaming,
128- Web3, and community building
129- </ p >
130270 </ div >
131- </ div >
132271
133- < div className = "grid grid-cols-1 lg:grid-cols-3 gap-8" >
134- { featuredPosts . map ( ( post ) => (
135- < FeaturedPostCard key = { post . id } post = { post } />
136- ) ) }
137- </ div >
138- </ section >
272+ < div className = "grid grid-cols-1 lg:grid-cols-3 gap-8" >
273+ { featuredPosts . map ( ( post ) => (
274+ < FeaturedPostCard key = { post . id } post = { post } />
275+ ) ) }
276+ </ div >
277+ </ section >
278+ ) }
139279
140280 { /* Category Filter */ }
141281 < section className = "mb-12" >
142282 < div className = "flex items-center justify-between mb-8" >
143- < h3 className = "text-2xl font-bold text-gray-900" > All Articles</ h3 >
283+ < h3 className = "text-2xl font-bold text-gray-900" >
284+ All Articles ({ allPosts . filter ( ( post ) => ! post . featured ) . length } )
285+ </ h3 >
144286 < div className = "flex items-center gap-2" >
145287 < Filter className = "w-4 h-4 text-gray-600" />
146288 < span className = "text-sm text-gray-600 font-medium" >
@@ -153,9 +295,10 @@ const BlogPage = async () => {
153295 { categories . map ( ( category ) => (
154296 < Button
155297 key = { category }
156- variant = { category === 'All' ? 'default' : 'outline' }
298+ variant = { category === selectedCategory ? 'default' : 'outline' }
157299 size = "sm"
158300 className = "rounded-lg"
301+ onClick = { ( ) => handleCategoryChange ( category ) }
159302 >
160303 { category }
161304 </ Button >
@@ -165,23 +308,44 @@ const BlogPage = async () => {
165308
166309 { /* Blog Posts Grid */ }
167310 < section >
168- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" >
169- { recentPosts . map ( ( post ) => (
170- < BlogPostCard key = { post . id } post = { post } />
171- ) ) }
172- </ div >
311+ { displayedPosts . length > 0 ? (
312+ < >
313+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" >
314+ { displayedPosts . map ( ( post ) => (
315+ < BlogPostCard key = { post . id } post = { post } />
316+ ) ) }
317+ </ div >
173318
174- { /* Load More Button */ }
175- < div className = "text-center mt-16" >
176- < Button
177- variant = "outline"
178- size = "lg"
179- className = "rounded-xl px-8 py-4 font-semibold border-slate-200 hover:bg-slate-50"
180- >
181- Load More Articles
182- < ArrowRight className = "w-4 h-4 ml-2" />
183- </ Button >
184- </ div >
319+ { /* Load More Button */ }
320+ { hasMorePosts ( ) && (
321+ < div className = "text-center mt-16" >
322+ < Button
323+ variant = "outline"
324+ size = "lg"
325+ className = "rounded-xl px-8 py-4 font-semibold border-slate-200 hover:bg-slate-50"
326+ onClick = { handleLoadMore }
327+ >
328+ Load More Articles
329+ < ArrowRight className = "w-4 h-4 ml-2" />
330+ </ Button >
331+ </ div >
332+ ) }
333+ </ >
334+ ) : (
335+ < div className = "text-center py-16" >
336+ < BookOpen className = "w-16 h-16 text-gray-300 mx-auto mb-4" />
337+ < h3 className = "text-xl font-semibold text-gray-600 mb-2" >
338+ No articles found
339+ </ h3 >
340+ < p className = "text-gray-500" >
341+ { searchTerm
342+ ? `No articles match "${ searchTerm } ". Try a different search term.`
343+ : selectedCategory !== 'All'
344+ ? `No articles found in the "${ selectedCategory } " category.`
345+ : 'No articles available at the moment.' }
346+ </ p >
347+ </ div >
348+ ) }
185349 </ section >
186350 </ div >
187351 </ div >
@@ -305,7 +469,4 @@ const BlogPostCard = ({ post }: { post: BlogPost }) => {
305469 ) ;
306470} ;
307471
308- // Force static generation for the blog page
309- export const dynamic = 'force-static' ;
310-
311472export default BlogPage ;
0 commit comments