From 0e0cb686fe8532a989a14979f5d02c990190297b Mon Sep 17 00:00:00 2001 From: JM-Flores Date: Thu, 13 Nov 2025 00:19:27 +0800 Subject: [PATCH 1/2] change paganation to infinite scroll --- .../app/home/feeds/_components/Feed.tsx | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/apps/codebility/app/home/feeds/_components/Feed.tsx b/apps/codebility/app/home/feeds/_components/Feed.tsx index 6ecca884c..15f057f82 100644 --- a/apps/codebility/app/home/feeds/_components/Feed.tsx +++ b/apps/codebility/app/home/feeds/_components/Feed.tsx @@ -1,7 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import DefaultPagination from "@/components/ui/pagination"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useFeedsStore } from "@/store/feeds-store"; import Post from "./PostCard"; @@ -15,7 +14,8 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) { const posts = useFeedsStore((state) => state.posts); const fetchPosts = useFeedsStore((state) => state.fetchPosts); - const [currentPage, setCurrentPage] = useState(1); + const [visibleCount, setVisibleCount] = useState(6); + const loaderRef = useRef(null); useEffect(() => { fetchPosts(); @@ -40,37 +40,48 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) { }); }, [posts, searchQuery]); - const postsPerPage = 6; - const totalPages = Math.ceil(filteredPosts.length / postsPerPage); - const startIndex = (currentPage - 1) * postsPerPage; - const currentPosts = filteredPosts.slice( - startIndex, - startIndex + postsPerPage, - ); + const visiblePosts = filteredPosts.slice(0, visibleCount); - // Reset to first page when search changes + // Reset visible count when search changes useEffect(() => { - setCurrentPage(1); + setVisibleCount(6); }, [searchQuery]); + // Infinite scroll logic + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + if (entry && entry.isIntersecting) { + setVisibleCount((prev) => { + if (prev < filteredPosts.length) { + return Math.min(prev + 6, filteredPosts.length); + } + return prev; + }); + } + }, + { threshold: 1 }, + ); + + const currentLoader = loaderRef.current; + if (currentLoader) observer.observe(currentLoader); + + return () => { + if (currentLoader) observer.unobserve(currentLoader); + }; + }, [filteredPosts.length]); + const handleDeletePost = (deletedPostId: string | number) => { useFeedsStore.setState((state) => ({ posts: state.posts.filter((post) => post.id !== deletedPostId), })); }; - const handleNextPage = () => { - if (currentPage < totalPages) setCurrentPage((p) => p + 1); - }; - - const handlePreviousPage = () => { - if (currentPage > 1) setCurrentPage((p) => p - 1); - }; - return ( <>
- {currentPosts.map((post) => ( + {visiblePosts.map((post) => ( - {filteredPosts.length > 0 && ( - + {/* Loader / sentinel div for infinite scroll */} + {visibleCount < filteredPosts.length && ( +
+
+
+ Loading more posts... +
+
+ )} + + {/* Empty state */} + {filteredPosts.length === 0 && ( +
No posts found.
)} ); From aa359ccad97d5ecc43021e38a5debcc45d08b45e Mon Sep 17 00:00:00 2001 From: JM-Flores Date: Thu, 13 Nov 2025 00:59:58 +0800 Subject: [PATCH 2/2] add sort menu --- .../app/home/feeds/_components/Feed.tsx | 68 +++++++++----- .../app/home/feeds/_services/query.ts | 34 ++++--- apps/codebility/app/home/feeds/page.tsx | 89 ++++++++++++++++--- 3 files changed, 146 insertions(+), 45 deletions(-) diff --git a/apps/codebility/app/home/feeds/_components/Feed.tsx b/apps/codebility/app/home/feeds/_components/Feed.tsx index 15f057f82..4a116cbb0 100644 --- a/apps/codebility/app/home/feeds/_components/Feed.tsx +++ b/apps/codebility/app/home/feeds/_components/Feed.tsx @@ -8,9 +8,16 @@ import Post from "./PostCard"; interface FeedProp { isAdmin: boolean; searchQuery?: string; + sortField: "title" | "date" | "upvotes"; + sortOrder: "asc" | "desc"; } -export default function Feed({ isAdmin, searchQuery }: FeedProp) { +export default function Feed({ + isAdmin, + searchQuery, + sortField, + sortOrder, +}: FeedProp) { const posts = useFeedsStore((state) => state.posts); const fetchPosts = useFeedsStore((state) => state.fetchPosts); @@ -21,12 +28,10 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) { fetchPosts(); }, [fetchPosts]); - // Filter posts by title or author's first/last name (case-insensitive) + // Filter posts const filteredPosts = useMemo(() => { if (!searchQuery || searchQuery.trim() === "") return posts; - const query = searchQuery.toLowerCase(); - return posts.filter((post) => { const titleMatch = post.title?.toLowerCase().includes(query); const firstNameMatch = post.author_id?.first_name @@ -35,30 +40,52 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) { const lastNameMatch = post.author_id?.last_name ?.toLowerCase() .includes(query); - return titleMatch || firstNameMatch || lastNameMatch; }); }, [posts, searchQuery]); - const visiblePosts = filteredPosts.slice(0, visibleCount); + // Sort posts + const sortedPosts = useMemo(() => { + const sorted = [...filteredPosts]; + sorted.sort((a, b) => { + let aVal: string | number = ""; + let bVal: string | number = ""; + + switch (sortField) { + case "title": + aVal = a.title?.toLowerCase() || ""; + bVal = b.title?.toLowerCase() || ""; + break; + case "date": + aVal = new Date(a.created_at).getTime(); + bVal = new Date(b.created_at).getTime(); + break; + case "upvotes": + aVal = a.upvote_count ?? 0; + bVal = b.upvote_count ?? 0; + break; + } + + if (aVal < bVal) return sortOrder === "asc" ? -1 : 1; + if (aVal > bVal) return sortOrder === "asc" ? 1 : -1; + return 0; + }); + return sorted; + }, [filteredPosts, sortField, sortOrder]); + + const visiblePosts = sortedPosts.slice(0, visibleCount); - // Reset visible count when search changes useEffect(() => { setVisibleCount(6); }, [searchQuery]); - // Infinite scroll logic + // Infinite scroll useEffect(() => { const observer = new IntersectionObserver( (entries) => { const entry = entries[0]; - if (entry && entry.isIntersecting) { - setVisibleCount((prev) => { - if (prev < filteredPosts.length) { - return Math.min(prev + 6, filteredPosts.length); - } - return prev; - }); + if (entry?.isIntersecting) { + setVisibleCount((prev) => Math.min(prev + 6, sortedPosts.length)); } }, { threshold: 1 }, @@ -66,12 +93,10 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) { const currentLoader = loaderRef.current; if (currentLoader) observer.observe(currentLoader); - return () => { if (currentLoader) observer.unobserve(currentLoader); }; - }, [filteredPosts.length]); - + }, [sortedPosts.length]); const handleDeletePost = (deletedPostId: string | number) => { useFeedsStore.setState((state) => ({ posts: state.posts.filter((post) => post.id !== deletedPostId), @@ -80,6 +105,7 @@ export default function Feed({ isAdmin, searchQuery }: FeedProp) { return ( <> + {/* Posts grid */}
{visiblePosts.map((post) => ( - {/* Loader / sentinel div for infinite scroll */} - {visibleCount < filteredPosts.length && ( + {/* Loader */} + {visibleCount < sortedPosts.length && (
No posts found.
)} diff --git a/apps/codebility/app/home/feeds/_services/query.ts b/apps/codebility/app/home/feeds/_services/query.ts index 3be509651..a5f679f64 100644 --- a/apps/codebility/app/home/feeds/_services/query.ts +++ b/apps/codebility/app/home/feeds/_services/query.ts @@ -5,22 +5,30 @@ import { createClientServerComponent } from "@/utils/supabase/server"; export const getPosts = async () => { try { const supabase = await createClientServerComponent(); - + const { data: posts, error } = await supabase - .from("posts") - .select(` - *, - author_id( - id, - first_name, - last_name, - image_url - ) - `).order('created_at', { ascending: false }); + .from("posts") + .select(` + *, + author_id ( + id, + first_name, + last_name, + image_url + ), + post_upvotes (id) + `) + .order("created_at", { ascending: false }); if (error) throw error; - return posts; + // compute upvote count based on the joined array + const postsWithUpvoteCount = posts.map((post) => ({ + ...post, + upvote_count: post.post_upvotes ? post.post_upvotes.length : 0, + })); + + return postsWithUpvoteCount; } catch (error) { console.error(error); throw error; @@ -39,5 +47,5 @@ export type PostType = { image_url?: string; }; image_url?: string; - upvoters_id?: string[]; + upvote_count?: number; }; diff --git a/apps/codebility/app/home/feeds/page.tsx b/apps/codebility/app/home/feeds/page.tsx index 1f64639f4..c840b5bd5 100644 --- a/apps/codebility/app/home/feeds/page.tsx +++ b/apps/codebility/app/home/feeds/page.tsx @@ -4,16 +4,23 @@ import { useEffect, useState } from "react"; import CreatePostModal from "@/components/modals/CreatePostModal"; import { Button } from "@/components/ui/button"; import { useUserStore } from "@/store/codev-store"; +import { ChevronDown } from "lucide-react"; import Feed from "./_components/Feed"; import SocialPointsCard from "./_components/SocialPointsCard"; import { getUserRole } from "./_services/action"; +type SortField = "title" | "date" | "upvotes"; +type SortOrder = "asc" | "desc"; + export default function FeedsPage() { const [isAdmin, setIsAdmin] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); + const [sortField, setSortField] = useState("date"); + const [sortOrder, setSortOrder] = useState("desc"); + const [sortMenuOpen, setSortMenuOpen] = useState(false); const { user } = useUserStore(); useEffect(() => { @@ -22,15 +29,11 @@ export default function FeedsPage() { const role = await getUserRole(user.role_id ?? null); setIsAdmin(role === "Admin"); }; - if (user) fetchRole(); }, [user]); useEffect(() => { - const handler = setTimeout(() => { - setDebouncedQuery(searchQuery); - }, 500); // triggers after 500ms of inactivity - + const handler = setTimeout(() => setDebouncedQuery(searchQuery), 500); return () => clearTimeout(handler); }, [searchQuery]); @@ -48,26 +51,90 @@ export default function FeedsPage() {

- {/* Social Points Card */} - {/* Search bar */} -
+ {/* Search bar + sort button row */} +
setSearchQuery(e.target.value)} - className="focus:border-customBlue-500 focus:ring-customBlue-500 dark:focus:border-customBlue-400 dark:focus:ring-customBlue-400 h-11 w-full rounded-lg border border-gray-300 bg-gray-50 px-4 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-1 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" + className="focus:border-customBlue-500 focus:ring-customBlue-500 h-11 flex-1 rounded-lg border border-gray-300 bg-gray-50 px-4 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-1 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" /> + +
+ + + {sortMenuOpen && ( +
+ {["date", "title", "upvotes"].map((field) => ( + + ))} +
+
+ Order: +
+ + +
+
+
+
+ )} +
- {/* Feed component with live search */} - +