11"use client" ;
22
3- import { useEffect , useMemo , useState } from "react" ;
4- import DefaultPagination from "@/components/ui/pagination" ;
3+ import { useEffect , useMemo , useRef , useState } from "react" ;
54import { useFeedsStore } from "@/store/feeds-store" ;
65
76import Post from "./PostCard" ;
87
98interface FeedProp {
109 isAdmin : boolean ;
1110 searchQuery ?: string ;
11+ sortField : "title" | "date" | "upvotes" ;
12+ sortOrder : "asc" | "desc" ;
1213}
1314
14- export default function Feed ( { isAdmin, searchQuery } : FeedProp ) {
15+ export default function Feed ( {
16+ isAdmin,
17+ searchQuery,
18+ sortField,
19+ sortOrder,
20+ } : FeedProp ) {
1521 const posts = useFeedsStore ( ( state ) => state . posts ) ;
1622 const fetchPosts = useFeedsStore ( ( state ) => state . fetchPosts ) ;
1723
18- const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
24+ const [ visibleCount , setVisibleCount ] = useState ( 6 ) ;
25+ const loaderRef = useRef < HTMLDivElement | null > ( null ) ;
1926
2027 useEffect ( ( ) => {
2128 fetchPosts ( ) ;
2229 } , [ fetchPosts ] ) ;
2330
24- // Filter posts by title or author's first/last name (case-insensitive)
31+ // Filter posts
2532 const filteredPosts = useMemo ( ( ) => {
2633 if ( ! searchQuery || searchQuery . trim ( ) === "" ) return posts ;
27-
2834 const query = searchQuery . toLowerCase ( ) ;
29-
3035 return posts . filter ( ( post ) => {
3136 const titleMatch = post . title ?. toLowerCase ( ) . includes ( query ) ;
3237 const firstNameMatch = post . author_id ?. first_name
@@ -35,42 +40,74 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) {
3540 const lastNameMatch = post . author_id ?. last_name
3641 ?. toLowerCase ( )
3742 . includes ( query ) ;
38-
3943 return titleMatch || firstNameMatch || lastNameMatch ;
4044 } ) ;
4145 } , [ posts , searchQuery ] ) ;
4246
43- const postsPerPage = 6 ;
44- const totalPages = Math . ceil ( filteredPosts . length / postsPerPage ) ;
45- const startIndex = ( currentPage - 1 ) * postsPerPage ;
46- const currentPosts = filteredPosts . slice (
47- startIndex ,
48- startIndex + postsPerPage ,
49- ) ;
47+ // Sort posts
48+ const sortedPosts = useMemo ( ( ) => {
49+ const sorted = [ ...filteredPosts ] ;
50+ sorted . sort ( ( a , b ) => {
51+ let aVal : string | number = "" ;
52+ let bVal : string | number = "" ;
53+
54+ switch ( sortField ) {
55+ case "title" :
56+ aVal = a . title ?. toLowerCase ( ) || "" ;
57+ bVal = b . title ?. toLowerCase ( ) || "" ;
58+ break ;
59+ case "date" :
60+ aVal = new Date ( a . created_at ) . getTime ( ) ;
61+ bVal = new Date ( b . created_at ) . getTime ( ) ;
62+ break ;
63+ case "upvotes" :
64+ aVal = a . upvote_count ?? 0 ;
65+ bVal = b . upvote_count ?? 0 ;
66+ break ;
67+ }
68+
69+ if ( aVal < bVal ) return sortOrder === "asc" ? - 1 : 1 ;
70+ if ( aVal > bVal ) return sortOrder === "asc" ? 1 : - 1 ;
71+ return 0 ;
72+ } ) ;
73+ return sorted ;
74+ } , [ filteredPosts , sortField , sortOrder ] ) ;
75+
76+ const visiblePosts = sortedPosts . slice ( 0 , visibleCount ) ;
5077
51- // Reset to first page when search changes
5278 useEffect ( ( ) => {
53- setCurrentPage ( 1 ) ;
79+ setVisibleCount ( 6 ) ;
5480 } , [ searchQuery ] ) ;
5581
82+ // Infinite scroll
83+ useEffect ( ( ) => {
84+ const observer = new IntersectionObserver (
85+ ( entries ) => {
86+ const entry = entries [ 0 ] ;
87+ if ( entry ?. isIntersecting ) {
88+ setVisibleCount ( ( prev ) => Math . min ( prev + 6 , sortedPosts . length ) ) ;
89+ }
90+ } ,
91+ { threshold : 1 } ,
92+ ) ;
93+
94+ const currentLoader = loaderRef . current ;
95+ if ( currentLoader ) observer . observe ( currentLoader ) ;
96+ return ( ) => {
97+ if ( currentLoader ) observer . unobserve ( currentLoader ) ;
98+ } ;
99+ } , [ sortedPosts . length ] ) ;
56100 const handleDeletePost = ( deletedPostId : string | number ) => {
57101 useFeedsStore . setState ( ( state ) => ( {
58102 posts : state . posts . filter ( ( post ) => post . id !== deletedPostId ) ,
59103 } ) ) ;
60104 } ;
61105
62- const handleNextPage = ( ) => {
63- if ( currentPage < totalPages ) setCurrentPage ( ( p ) => p + 1 ) ;
64- } ;
65-
66- const handlePreviousPage = ( ) => {
67- if ( currentPage > 1 ) setCurrentPage ( ( p ) => p - 1 ) ;
68- } ;
69-
70106 return (
71107 < >
108+ { /* Posts grid */ }
72109 < div className = "grid grid-cols-1 gap-4 md:grid-cols-3" >
73- { currentPosts . map ( ( post ) => (
110+ { visiblePosts . map ( ( post ) => (
74111 < Post
75112 key = { post . id }
76113 post = { post }
@@ -80,14 +117,22 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) {
80117 ) ) }
81118 </ div >
82119
83- { filteredPosts . length > 0 && (
84- < DefaultPagination
85- totalPages = { totalPages }
86- currentPage = { currentPage }
87- handleNextPage = { handleNextPage }
88- handlePreviousPage = { handlePreviousPage }
89- setCurrentPage = { setCurrentPage }
90- />
120+ { /* Loader */ }
121+ { visibleCount < sortedPosts . length && (
122+ < div
123+ ref = { loaderRef }
124+ className = "mt-6 flex justify-center text-sm text-gray-500"
125+ >
126+ < div className = "flex items-center gap-2" >
127+ < div className = "h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
128+ Loading more posts...
129+ </ div >
130+ </ div >
131+ ) }
132+
133+ { /* Empty state */ }
134+ { sortedPosts . length === 0 && (
135+ < div className = "mt-8 text-center text-gray-500" > No posts found.</ div >
91136 ) }
92137 </ >
93138 ) ;
0 commit comments