Skip to content

Commit 0db7432

Browse files
committed
feat: Refactor BlogPage to use client-side data fetching and enhance user interaction
- Converted BlogPage to a client component, enabling dynamic data fetching for featured posts, all posts, and categories. - Implemented state management for posts, categories, and search functionality, improving user experience. - Added filtering and search capabilities for blog posts based on category and search term. - Enhanced loading state and error handling during data fetching. - Updated UI to reflect the number of articles available and improved the layout for better readability.
1 parent 2a9e931 commit 0db7432

File tree

2 files changed

+248
-53
lines changed

2 files changed

+248
-53
lines changed

packages/app/app/api/blog/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import {
3+
getSortedPostsData,
4+
getFeaturedPosts,
5+
getAllCategories,
6+
} from '@/lib/utils/blog';
7+
8+
export async function GET(request: NextRequest) {
9+
try {
10+
const { searchParams } = new URL(request.url);
11+
const type = searchParams.get('type');
12+
13+
switch (type) {
14+
case 'featured':
15+
const featuredPosts = await getFeaturedPosts();
16+
return NextResponse.json({ data: featuredPosts });
17+
18+
case 'categories':
19+
const categories = await getAllCategories();
20+
return NextResponse.json({ data: categories });
21+
22+
case 'all':
23+
default:
24+
const allPosts = await getSortedPostsData();
25+
return NextResponse.json({ data: allPosts });
26+
}
27+
} catch (error) {
28+
console.error('Error fetching blog data:', error);
29+
return NextResponse.json(
30+
{ error: 'Failed to fetch blog data' },
31+
{ status: 500 }
32+
);
33+
}
34+
}

packages/app/app/blog/page.tsx

Lines changed: 214 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Suspense } from 'react';
1+
'use client';
2+
3+
import { Suspense, useState, useEffect } from 'react';
24
import { Button } from '@/components/ui/button';
35
import { Card, CardContent, CardHeader } from '@/components/ui/card';
46
import { Badge } from '@/components/ui/badge';
@@ -15,12 +17,7 @@ import {
1517
} from 'lucide-react';
1618
import Link from 'next/link';
1719
import 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
2623
const 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-
311472
export default BlogPage;

0 commit comments

Comments
 (0)