Skip to content

Commit f54ba77

Browse files
committed
feat: refactor Posts and Projects components for improved data fetching and loading experience
1 parent 82a98e9 commit f54ba77

2 files changed

Lines changed: 128 additions & 160 deletions

File tree

src/app/posts/page.tsx

Lines changed: 64 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
22

3-
import React, { useEffect, useState, useCallback, useRef, useMemo, Suspense } from "react";
4-
import InfiniteScroll from "@components/infinit-scroll";
3+
import React, { useEffect, useState, useCallback, useMemo, Suspense } from "react";
54
import { Spinner } from "@components/ui/loading";
65
import { PostCard } from "@components/cards/post-card";
76
import { Post } from "../../lib/types/interfaces";
@@ -11,105 +10,71 @@ import { PostsHero } from "@components/heros/posts-hero";
1110
import { useSearchParams, useRouter } from "next/navigation";
1211
import { cn } from "@lib/utils";
1312
import { Button } from "src/components/ui";
13+
import Footer from '../../components/layouts/footer';
14+
15+
const ITEMS_PER_PAGE = 12;
1416

1517
const PostsContent = () => {
1618
const router = useRouter();
1719
const searchParams = useSearchParams();
18-
const limit = 9;
19-
const [page, setPage] = useState(1);
2020
const [loading, setLoading] = useState(false);
21-
const [hasMore, setHasMore] = useState(true);
22-
const [posts, setPosts] = useState<Post[]>([]);
21+
const [loadingMore, setLoadingMore] = useState(false);
22+
const [allPosts, setAllPosts] = useState<Post[]>([]);
23+
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
24+
const [availableTags, setAvailableTags] = useState<string[]>([]);
2325
const [searchQuery, setSearchQuery] = useState("");
2426
const [selectedTag, setSelectedTag] = useState("");
25-
const [availableTags, setAvailableTags] = useState<string[]>([]);
26-
const hasFetched = useRef(false);
27-
const isFetchingRef = useRef(false);
28-
29-
const next = useCallback(async () => {
30-
if (isFetchingRef.current || !hasMore) return;
31-
32-
isFetchingRef.current = true;
33-
setLoading(true);
34-
const currentPage = page;
35-
36-
try {
37-
const response = await fetch(`/api/posts?page=${currentPage}&limit=${limit}`);
38-
39-
if (!response.ok) {
40-
throw new Error(`HTTP error! status: ${response.status}`);
41-
}
42-
43-
const { data, hasMore: apiHasMore, tags } = await response.json();
44-
45-
setPosts((prev) => {
46-
const existingSlugs = new Set(prev.map(p => p.slug));
47-
const newPosts = (data as Post[]).filter(p => !existingSlugs.has(p.slug));
48-
return [...prev, ...newPosts];
49-
});
50-
setPage((prev) => prev + 1);
51-
setHasMore(apiHasMore);
52-
if (Array.isArray(tags)) {
53-
setAvailableTags(tags);
54-
}
55-
56-
} catch (error) {
57-
console.error('Error fetching posts:', error);
58-
setHasMore(false);
59-
} finally {
60-
isFetchingRef.current = false;
61-
setLoading(false);
62-
}
63-
}, [hasMore, page, limit]);
6427

28+
// Fetch all posts once on mount
6529
useEffect(() => {
66-
if (!hasFetched.current) {
67-
hasFetched.current = true;
68-
next();
69-
}
70-
// eslint-disable-next-line react-hooks/exhaustive-deps
30+
const fetchAll = async () => {
31+
setLoading(true);
32+
try {
33+
const response = await fetch(`/api/posts?page=1&limit=999`);
34+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
35+
const { data, tags } = await response.json();
36+
setAllPosts(data as Post[]);
37+
if (Array.isArray(tags)) setAvailableTags(tags);
38+
} catch (error) {
39+
console.error('Error fetching posts:', error);
40+
} finally {
41+
setLoading(false);
42+
}
43+
};
44+
fetchAll();
7145
}, []);
7246

73-
// Sync search query with URL params on mount and when URL changes
47+
// Sync search/tag from URL and reset visible count
7448
useEffect(() => {
7549
const queryParam = searchParams.get('q') || "";
7650
const tagParam = searchParams.get('tag') || "";
7751
setSearchQuery(queryParam);
7852
setSelectedTag(tagParam);
53+
setVisibleCount(ITEMS_PER_PAGE);
7954
}, [searchParams]);
8055

8156
const filteredPosts = useMemo(() => {
8257
const query = searchQuery.toLowerCase().trim();
8358
const normalizedTag = selectedTag.toLowerCase();
84-
85-
return posts.filter((post) => {
59+
return allPosts.filter((post) => {
8660
const matchesSearch = !query ||
8761
post.title?.toLowerCase().includes(query) ||
8862
post.description?.toLowerCase().includes(query) ||
8963
post.tags?.some(tag => tag.toLowerCase().includes(query));
90-
9164
const matchesTag = !normalizedTag ||
9265
post.tags?.some(tag => tag.toLowerCase() === normalizedTag);
93-
9466
return matchesSearch && matchesTag;
9567
});
96-
}, [posts, searchQuery, selectedTag]);
68+
}, [allPosts, searchQuery, selectedTag]);
69+
70+
const visiblePosts = filteredPosts.slice(0, visibleCount);
71+
const hasMore = visibleCount < filteredPosts.length;
9772

9873
const updateSearchParams = useCallback((query: string, tag: string) => {
9974
const params = new URLSearchParams(searchParams.toString());
100-
101-
if (query.trim()) {
102-
params.set('q', query);
103-
} else {
104-
params.delete('q');
105-
}
106-
107-
if (tag.trim()) {
108-
params.set('tag', tag);
109-
} else {
110-
params.delete('tag');
111-
}
112-
75+
if (query.trim()) { params.set('q', query); } else { params.delete('q'); }
76+
if (tag.trim()) { params.set('tag', tag); } else { params.delete('tag'); }
77+
params.delete('page');
11378
const queryString = params.toString();
11479
router.push(`/posts${queryString ? `?${queryString}` : ''}`, { scroll: false });
11580
}, [router, searchParams]);
@@ -129,6 +94,14 @@ const PostsContent = () => {
12994
updateSearchParams("", selectedTag);
13095
}, [selectedTag, updateSearchParams]);
13196

97+
const handleLoadMore = useCallback(() => {
98+
setLoadingMore(true);
99+
setTimeout(() => {
100+
setVisibleCount((prev) => prev + ITEMS_PER_PAGE);
101+
setLoadingMore(false);
102+
}, 300);
103+
}, []);
104+
132105
return (
133106
<main className="w-full flex flex-col gap-7 pb-5">
134107
<NavigationBar />
@@ -179,8 +152,15 @@ const PostsContent = () => {
179152
</div>
180153
)} */}
181154
<article className="grid max-w-5xl mx-auto p-5 grid-cols-1 xs:grid-cols-2 lg:grid-cols-3 gap-4 min-h-75 relative">
182-
{filteredPosts.map((post) => (<PostCard key={post.id || post.slug} post={post} />))}
183-
{filteredPosts.length === 0 && (searchQuery || selectedTag) && (
155+
{loading && (
156+
<div className="col-span-full flex justify-center items-center py-12">
157+
<Spinner variant={'bars'} />
158+
</div>
159+
)}
160+
{!loading && visiblePosts.map((post) => (
161+
<PostCard key={post.id || post.slug} post={post} />
162+
))}
163+
{!loading && filteredPosts.length === 0 && (searchQuery || selectedTag) && (
184164
<div className="col-span-full text-center py-12">
185165
<p className="text-muted-foreground text-lg">
186166
No blogs found
@@ -189,14 +169,18 @@ const PostsContent = () => {
189169
</p>
190170
</div>
191171
)}
192-
<InfiniteScroll hasMore={hasMore} isLoading={loading} next={next} threshold={1}>
193-
{hasMore && (
194-
<div className='col-span-full flex justify-center items-center'>
195-
<Spinner variant={'bars'} />
196-
</div>
197-
)}
198-
</InfiniteScroll>
199172
</article>
173+
{!loading && hasMore && (
174+
<div className="flex justify-center px-5 mt-2 pb-4">
175+
<button
176+
onClick={handleLoadMore}
177+
disabled={loadingMore}
178+
className="flex items-center gap-2 px-6 py-2 rounded-full border border-input bg-background text-sm hover:bg-accent transition-colors disabled:opacity-40 disabled:pointer-events-none"
179+
>
180+
{loadingMore ? <Spinner variant="bars" /> : "Load more"}
181+
</button>
182+
</div>
183+
)}
200184
</BlurFade>
201185
</main>
202186
);
@@ -212,7 +196,8 @@ const Posts = () => {
212196
</div>
213197
</main>
214198
}>
215-
<PostsContent />
199+
<PostsContent />
200+
<Footer />
216201
</Suspense>
217202
);
218203
};

0 commit comments

Comments
 (0)