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" ;
54import { Spinner } from "@components/ui/loading" ;
65import { PostCard } from "@components/cards/post-card" ;
76import { Post } from "../../lib/types/interfaces" ;
@@ -11,105 +10,71 @@ import { PostsHero } from "@components/heros/posts-hero";
1110import { useSearchParams , useRouter } from "next/navigation" ;
1211import { cn } from "@lib/utils" ;
1312import { Button } from "src/components/ui" ;
13+ import Footer from '../../components/layouts/footer' ;
14+
15+ const ITEMS_PER_PAGE = 12 ;
1416
1517const 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