From 9f6b9eb5e0bef0a6c37b47eef1afae599a715605 Mon Sep 17 00:00:00 2001 From: JM-Flores Date: Mon, 17 Nov 2025 15:24:44 +0800 Subject: [PATCH 1/8] add comments count --- .../app/home/feeds/_components/PostCard.tsx | 11 ++++++++++- apps/codebility/app/home/feeds/_services/query.ts | 12 ++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/codebility/app/home/feeds/_components/PostCard.tsx b/apps/codebility/app/home/feeds/_components/PostCard.tsx index 022b4eab0..f5a07c93f 100644 --- a/apps/codebility/app/home/feeds/_components/PostCard.tsx +++ b/apps/codebility/app/home/feeds/_components/PostCard.tsx @@ -6,7 +6,7 @@ import { Box } from "@/components/shared/dashboard"; import { defaultAvatar } from "@/public/assets/images"; import { useUserStore } from "@/store/codev-store"; import { format } from "date-fns"; -import { ArrowBigUp } from "lucide-react"; +import { ArrowBigUp, MessageSquare } from "lucide-react"; import toast from "react-hot-toast"; import { deletePost } from "../_services/action"; @@ -124,6 +124,15 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
+ + {/* Comment Count */} +
+ + {post.comment_count ?? 0} +
{ last_name, image_url ), - post_upvotes (id) + post_upvotes (id), + post_comments (id) `) .order("created_at", { ascending: false }); if (error) throw error; - // compute upvote count based on the joined array - const postsWithUpvoteCount = posts.map((post) => ({ + // compute upvote & comment counts + const postsWithCounts = posts.map((post) => ({ ...post, upvote_count: post.post_upvotes ? post.post_upvotes.length : 0, + comment_count: post.post_comments ? post.post_comments.length : 0, })); - return postsWithUpvoteCount; + return postsWithCounts; } catch (error) { console.error(error); throw error; } }; + export type PostType = { id: string; created_at: string; @@ -48,4 +51,5 @@ export type PostType = { }; image_url?: string; upvote_count?: number; + comment_count?: number; }; From f25138d9bda3f5f5f23aea0807f62758e9389aa9 Mon Sep 17 00:00:00 2001 From: JM-Flores Date: Mon, 17 Nov 2025 15:27:44 +0800 Subject: [PATCH 2/8] add sort by comments --- apps/codebility/app/home/feeds/_components/Feed.tsx | 6 +++++- apps/codebility/app/home/feeds/page.tsx | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/codebility/app/home/feeds/_components/Feed.tsx b/apps/codebility/app/home/feeds/_components/Feed.tsx index 4a116cbb0..d9f178fba 100644 --- a/apps/codebility/app/home/feeds/_components/Feed.tsx +++ b/apps/codebility/app/home/feeds/_components/Feed.tsx @@ -8,7 +8,7 @@ import Post from "./PostCard"; interface FeedProp { isAdmin: boolean; searchQuery?: string; - sortField: "title" | "date" | "upvotes"; + sortField: "title" | "date" | "upvotes" | "comments"; sortOrder: "asc" | "desc"; } @@ -64,6 +64,10 @@ export default function Feed({ aVal = a.upvote_count ?? 0; bVal = b.upvote_count ?? 0; break; + case "comments": + aVal = a.comment_count ?? 0; + bVal = b.comment_count ?? 0; + break; } if (aVal < bVal) return sortOrder === "asc" ? -1 : 1; diff --git a/apps/codebility/app/home/feeds/page.tsx b/apps/codebility/app/home/feeds/page.tsx index c840b5bd5..20e32878e 100644 --- a/apps/codebility/app/home/feeds/page.tsx +++ b/apps/codebility/app/home/feeds/page.tsx @@ -83,7 +83,7 @@ export default function FeedsPage() { {sortMenuOpen && (
- {["date", "title", "upvotes"].map((field) => ( + {["date", "title", "upvotes", "comments"].map((field) => ( - - {sortMenuOpen && ( -
- {["date", "title", "upvotes", "comments"].map((field) => ( - - ))} -
-
- Order: -
- - -
-
-
-
- )} -
+ Date: Mon, 17 Nov 2025 15:52:53 +0800 Subject: [PATCH 6/8] sort menu --- .../app/home/feeds/_components/SortMenu.tsx | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 apps/codebility/app/home/feeds/_components/SortMenu.tsx diff --git a/apps/codebility/app/home/feeds/_components/SortMenu.tsx b/apps/codebility/app/home/feeds/_components/SortMenu.tsx new file mode 100644 index 000000000..7bcf016d9 --- /dev/null +++ b/apps/codebility/app/home/feeds/_components/SortMenu.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { ChevronDown } from "lucide-react"; + +type SortField = "title" | "date" | "upvotes"; +type SortOrder = "asc" | "desc"; + +interface SortMenuProps { + sortField: SortField; + sortOrder: SortOrder; + onChangeSortField: (field: SortField) => void; + onChangeSortOrder: (order: SortOrder) => void; +} + +export default function SortMenu({ + sortField, + sortOrder, + onChangeSortField, + onChangeSortOrder, +}: SortMenuProps) { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + // Close dropdown on outside click + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+ + + {open && ( +
+ {["date", "title", "upvotes", "comments"].map((field) => ( + + ))} + +
+
+ Order: + +
+ + + +
+
+
+
+ )} +
+ ); +} From 52fe0d03ff6c5708be3934ad77957029727a32de Mon Sep 17 00:00:00 2001 From: JM-Flores Date: Mon, 17 Nov 2025 15:53:17 +0800 Subject: [PATCH 7/8] add loading skeleton for feeds post cards --- .../app/home/feeds/_components/Feed.tsx | 26 ++++++++++++------- .../feeds/_components/PostCardSkeleton.tsx | 20 ++++++++++++++ apps/codebility/store/feeds-store.ts | 9 +++++-- 3 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 apps/codebility/app/home/feeds/_components/PostCardSkeleton.tsx diff --git a/apps/codebility/app/home/feeds/_components/Feed.tsx b/apps/codebility/app/home/feeds/_components/Feed.tsx index 05b0ac04a..ea94e74e3 100644 --- a/apps/codebility/app/home/feeds/_components/Feed.tsx +++ b/apps/codebility/app/home/feeds/_components/Feed.tsx @@ -5,6 +5,7 @@ import { useFeedsStore } from "@/store/feeds-store"; import { POSTS_PER_PAGE } from "../_constants"; import Post from "./PostCard"; +import PostCardSkeleton from "./PostCardSkeleton"; interface FeedProp { isAdmin: boolean; @@ -21,6 +22,7 @@ export default function Feed({ }: FeedProp) { const posts = useFeedsStore((state) => state.posts); const fetchPosts = useFeedsStore((state) => state.fetchPosts); + const isLoading = useFeedsStore((state) => state.isLoading); const [visibleCount, setVisibleCount] = useState(POSTS_PER_PAGE); const loaderRef = useRef(null); @@ -114,18 +116,22 @@ export default function Feed({ <> {/* Posts grid */}
- {visiblePosts.map((post) => ( - - ))} + {isLoading + ? Array.from({ length: 6 }).map((_, i) => ( + + )) + : visiblePosts.map((post) => ( + + ))}
{/* Loader */} - {visibleCount < sortedPosts.length && ( + {!isLoading && visibleCount < sortedPosts.length && (
No posts found.
)} diff --git a/apps/codebility/app/home/feeds/_components/PostCardSkeleton.tsx b/apps/codebility/app/home/feeds/_components/PostCardSkeleton.tsx new file mode 100644 index 000000000..7a7179fe4 --- /dev/null +++ b/apps/codebility/app/home/feeds/_components/PostCardSkeleton.tsx @@ -0,0 +1,20 @@ +export default function PostCardSkeleton() { + return ( +
+ {/* Image */} +
+ + {/* Title */} +
+ + {/* Subtitle */} +
+ + {/* Footer badges */} +
+
+
+
+
+ ); +} diff --git a/apps/codebility/store/feeds-store.ts b/apps/codebility/store/feeds-store.ts index 2326bd89b..249f920a3 100644 --- a/apps/codebility/store/feeds-store.ts +++ b/apps/codebility/store/feeds-store.ts @@ -1,17 +1,22 @@ -import {create} from "zustand"; +import { create } from "zustand"; import { getPosts, PostType } from "../app/home/feeds/_services/query"; interface FeedsState { posts: PostType[]; + isLoading: boolean; fetchPosts: () => Promise; setPosts: (posts: PostType[]) => void; } export const useFeedsStore = create((set) => ({ posts: [], + isLoading: true, + fetchPosts: async () => { + set({ isLoading: true }); const posts = await getPosts(); - set({ posts }); + set({ posts, isLoading: false }); }, + setPosts: (posts) => set({ posts }), })); From decb61d9529ee215e7b96bd4adc861b289c9027a Mon Sep 17 00:00:00 2001 From: JM-Flores Date: Mon, 17 Nov 2025 17:40:25 +0800 Subject: [PATCH 8/8] fix loading, revalidation, comment count --- .../codebility/app/home/feeds/IMPROVEMENTS.md | 56 ++++++------- .../app/home/feeds/_components/Feed.tsx | 10 +-- .../app/home/feeds/_components/PostCard.tsx | 10 +-- .../feeds/_components/PostCommentCount.tsx | 25 ++++++ .../app/home/feeds/_components/PostUpvote.tsx | 10 ++- .../app/home/feeds/_components/PostView.tsx | 80 +++++-------------- .../feeds/_components/PostViewCommentItem.tsx | 13 ++- .../feeds/_components/PostViewCommentList.tsx | 1 + .../_components/PostViewCreateComment.tsx | 20 +++-- apps/codebility/store/feeds-store.ts | 16 +++- 10 files changed, 124 insertions(+), 117 deletions(-) create mode 100644 apps/codebility/app/home/feeds/_components/PostCommentCount.tsx diff --git a/apps/codebility/app/home/feeds/IMPROVEMENTS.md b/apps/codebility/app/home/feeds/IMPROVEMENTS.md index 04a6d603f..3ec14d4de 100644 --- a/apps/codebility/app/home/feeds/IMPROVEMENTS.md +++ b/apps/codebility/app/home/feeds/IMPROVEMENTS.md @@ -4,10 +4,10 @@ This document outlines potential improvements for the feeds feature based on cod ## Performance & Optimization -1. **Implement infinite scroll** instead of traditional pagination + 2. **Add optimistic updates** for upvotes/comments - Instant UI feedback before server response @@ -19,17 +19,12 @@ This document outlines potential improvements for the feeds feature based on cod - Automatic background refetching - Built-in stale-while-revalidate pattern -4. **Add real-time updates** using Supabase subscriptions - - New posts appear automatically without manual refresh - - Live upvote/comment count updates - - Better collaboration experience - -5. **Optimize image loading** +4. **Optimize image loading** - Add blur placeholders (Next.js Image component supports this) - Lazy loading for images below the fold - Progressive image loading -6. **Memoize filtered posts computation** +5. **Memoize filtered posts computation** - Already using `useMemo` in Feed.tsx:25-41 - Could optimize further with better dependency keys - Consider moving heavy filtering to server-side @@ -41,11 +36,11 @@ This document outlines potential improvements for the feeds feature based on cod - Multiple categories per post - Visual category badges -8. **Sort options** + 9. **Bookmark/save posts** - Let users save posts for later reading @@ -57,10 +52,10 @@ This document outlines potential improvements for the feeds feature based on cod - Show content excerpt with "Read more..." - Better preview of post content -11. **Comment count display** on PostCard + 12. **Share functionality** - Copy link to clipboard @@ -72,22 +67,17 @@ This document outlines potential improvements for the feeds feature based on cod - Notify mentioned users - Highlight mentions in text -14. **Image gallery view** for posts with multiple images - - Currently supports single thumbnail - - Carousel/gallery for multiple images - - Lightbox for full-size viewing - -15. **Draft posts** +14. **Draft posts** - Save work in progress before publishing - Auto-save functionality - Resume editing later ## Code Quality & Architecture -16. **Fix hardcoded admin check** (page.tsx:13) + 17. **Consolidate duplicate user role fetching** - Used in page.tsx:20-27 and PostView.tsx:48-52 @@ -110,15 +100,15 @@ This document outlines potential improvements for the feeds feature based on cod - Graceful degradation - User-friendly error messages -21. **Loading skeletons** for individual post cards + -22. **Extract magic numbers** to constants + 23. **Consistent async/await error handling** - Some functions use try/catch, others don't @@ -234,29 +224,29 @@ This document outlines potential improvements for the feeds feature based on cod These improvements can be implemented quickly and provide immediate value: -- ✅ **Fix the `isAdmin` initialization bug** (page.tsx:13) + -- ✅ **Extract constants** + -- ✅ **Add loading skeleton for post cards** + -- ✅ **Add empty state when no posts match search** + ## File References diff --git a/apps/codebility/app/home/feeds/_components/Feed.tsx b/apps/codebility/app/home/feeds/_components/Feed.tsx index ea94e74e3..5bba3527b 100644 --- a/apps/codebility/app/home/feeds/_components/Feed.tsx +++ b/apps/codebility/app/home/feeds/_components/Feed.tsx @@ -22,14 +22,14 @@ export default function Feed({ }: FeedProp) { const posts = useFeedsStore((state) => state.posts); const fetchPosts = useFeedsStore((state) => state.fetchPosts); - const isLoading = useFeedsStore((state) => state.isLoading); + const isFetchingPosts = useFeedsStore((state) => state.isFetchingPosts); const [visibleCount, setVisibleCount] = useState(POSTS_PER_PAGE); const loaderRef = useRef(null); useEffect(() => { fetchPosts(); - }, [fetchPosts]); + }, []); // Filter posts const filteredPosts = useMemo(() => { @@ -116,7 +116,7 @@ export default function Feed({ <> {/* Posts grid */}
- {isLoading + {isFetchingPosts ? Array.from({ length: 6 }).map((_, i) => ( )) @@ -131,7 +131,7 @@ export default function Feed({
{/* Loader */} - {!isLoading && visibleCount < sortedPosts.length && ( + {!isFetchingPosts && visibleCount < sortedPosts.length && (
No posts found.
)} diff --git a/apps/codebility/app/home/feeds/_components/PostCard.tsx b/apps/codebility/app/home/feeds/_components/PostCard.tsx index f5a07c93f..183685546 100644 --- a/apps/codebility/app/home/feeds/_components/PostCard.tsx +++ b/apps/codebility/app/home/feeds/_components/PostCard.tsx @@ -12,6 +12,7 @@ import toast from "react-hot-toast"; import { deletePost } from "../_services/action"; import { PostType } from "../_services/query"; import { DeleteDialog } from "./DeleteDialog"; +import PostCommentCount from "./PostCommentCount"; import PostUpvote from "./PostUpvote"; interface PostProps { @@ -125,14 +126,7 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
- {/* Comment Count */} -
- - {post.comment_count ?? 0} -
+
{ + setCount(post.comment_count ?? 0); + }, [post]); + + return ( +
+ + {count} +
+ ); +} diff --git a/apps/codebility/app/home/feeds/_components/PostUpvote.tsx b/apps/codebility/app/home/feeds/_components/PostUpvote.tsx index f79a94a98..775c11bd1 100644 --- a/apps/codebility/app/home/feeds/_components/PostUpvote.tsx +++ b/apps/codebility/app/home/feeds/_components/PostUpvote.tsx @@ -63,11 +63,17 @@ export default function PostUpvote({ post }: PostUpvoteProps) { if (!isUpvoted) { const postUpvote = await AddPostUpvote(post.id, user.id); + + useFeedsStore.getState().updatePost(post.id, { + upvote_count: post.comment_count! + 1, + }); } else { await removePostUpvote(post.id, user.id); - } - await fetchPosts(); + useFeedsStore.getState().updatePost(post.id, { + upvote_count: post.comment_count! - 1, + }); + } } }; diff --git a/apps/codebility/app/home/feeds/_components/PostView.tsx b/apps/codebility/app/home/feeds/_components/PostView.tsx index 69122f12d..007f5c1bd 100644 --- a/apps/codebility/app/home/feeds/_components/PostView.tsx +++ b/apps/codebility/app/home/feeds/_components/PostView.tsx @@ -2,14 +2,13 @@ import { useEffect, useState } from "react"; import Image from "next/image"; -import { useParams } from "next/navigation"; import EditPostModal from "@/components/modals/EditPostModal"; import { Box } from "@/components/shared/dashboard"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton/skeleton"; import { defaultAvatar } from "@/public/assets/images"; import { useUserStore } from "@/store/codev-store"; -import { createClientClientComponent } from "@/utils/supabase/client"; +import { useFeedsStore } from "@/store/feeds-store"; import { format } from "date-fns"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; @@ -17,6 +16,7 @@ import remarkGfm from "remark-gfm"; import type { PostType } from "../_services/query"; import PostUpvote from "../_components/PostUpvote"; import { getUserRole } from "../_services/action"; +import PostCommentCount from "./PostCommentCount"; import PostViewCommentList from "./PostViewCommentList"; import PostViewCreateComment from "./PostViewCreateComment"; @@ -25,25 +25,27 @@ interface PostViewProps { } export default function PostView({ postId }: PostViewProps) { - const [supabase, setSupabase] = useState(null); - - useEffect(() => { - const supabaseClient = createClientClientComponent(); - setSupabase(supabaseClient); - }, []); - const [post, setPost] = useState(null); - const [error, setError] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isAdmin, setIsAdmin] = useState(true); const [isAuthor, setIsAuthor] = useState(false); const { user } = useUserStore(); const [refreshComments, setRefreshComments] = useState(0); + const { posts } = useFeedsStore(); + + // Trigger comment refresh const triggerRefreshComments = () => { setRefreshComments((prev) => prev + 1); }; + // Find the post in the store + useEffect(() => { + const foundPost = posts.find((p) => p.id === postId) || null; + setPost(foundPost); + }, [posts, postId]); + + // Check roles useEffect(() => { const fetchRole = async () => { if (!user) return; @@ -51,18 +53,9 @@ export default function PostView({ postId }: PostViewProps) { setIsAdmin(role === "Admin"); }; - const checkIfAuthor = async () => { - console.log( - "CHECKING USER ID AND AUTHOR ID: ", - user?.id, - post?.author_id?.id, - ); + const checkIfAuthor = () => { if (!user) return; - if (user.id === post?.author_id?.id) { - setIsAuthor(true); - } else { - setIsAuthor(false); - } + setIsAuthor(user.id === post?.author_id?.id); }; if (user) { @@ -71,40 +64,6 @@ export default function PostView({ postId }: PostViewProps) { } }, [user, post]); - const fetchPost = async () => { - if (!supabase) return; - try { - const { data, error } = await supabase - .from("posts") - .select( - `*, - author_id( - id, - first_name, - last_name, - image_url - )`, - ) - .eq("id", postId) - .single(); - - if (error) throw error; - setPost(data || null); - } catch (error) { - console.error("Error fetching post:", error); - } - }; - - useEffect(() => { - if (supabase) { - fetchPost(); - } - }, [postId, supabase]); - - if (error) { - return
Error loading post: {error}
; - } - if (!post) { return (
@@ -141,9 +100,11 @@ export default function PostView({ postId }: PostViewProps) {

- +
+ + +
- {/* Scrollable group */}
@@ -156,6 +117,7 @@ export default function PostView({ postId }: PostViewProps) { Edit Post )} +
@@ -172,7 +134,7 @@ export default function PostView({ postId }: PostViewProps) { post={post} isOpen={isModalOpen} onClose={closeModal} - onPostUpdated={fetchPost} + onPostUpdated={() => {}} /> ); diff --git a/apps/codebility/app/home/feeds/_components/PostViewCommentItem.tsx b/apps/codebility/app/home/feeds/_components/PostViewCommentItem.tsx index 05c717295..695cec5c5 100644 --- a/apps/codebility/app/home/feeds/_components/PostViewCommentItem.tsx +++ b/apps/codebility/app/home/feeds/_components/PostViewCommentItem.tsx @@ -6,8 +6,10 @@ import { useFeedsStore } from "@/store/feeds-store"; import { X } from "lucide-react"; import { deletePostComment } from "../_services/action"; +import { PostType } from "../_services/query"; interface PostViewCommentItemProps { + postId: string; commenntId: string; userImage: string; userName: string; @@ -16,6 +18,7 @@ interface PostViewCommentItemProps { } export default function PostViewCommentItem({ + postId, commenntId, userImage, userName, @@ -29,7 +32,15 @@ export default function PostViewCommentItem({ setIsVisible(false); try { await deletePostComment(commenntId); - fetchPosts(); + // Get current post from store + const currentPost = useFeedsStore + .getState() + .posts.find((p) => p.id === postId); + if (currentPost) { + useFeedsStore.getState().updatePost(postId, { + comment_count: (currentPost.comment_count ?? 0) - 1, + }); + } } catch (error) { setIsVisible(true); } diff --git a/apps/codebility/app/home/feeds/_components/PostViewCommentList.tsx b/apps/codebility/app/home/feeds/_components/PostViewCommentList.tsx index 331f27b07..ff0b92276 100644 --- a/apps/codebility/app/home/feeds/_components/PostViewCommentList.tsx +++ b/apps/codebility/app/home/feeds/_components/PostViewCommentList.tsx @@ -65,6 +65,7 @@ export default function PostViewCommentList({ {comments.map((c) => ( void; } export default function PostViewCreateComment({ - postId, + post, onCommentCreated, }: CreateCommentProps) { const [comment, setComment] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - const fetchPosts = useFeedsStore((state) => state.fetchPosts); const { user } = useUserStore(); const handleKeyDown = async (e: React.KeyboardEvent) => { if (e.key === "Enter" && comment.trim() && !isSubmitting) { if (!user || !user.id) return; + try { setIsSubmitting(true); - await createComment(postId, user.id, comment); + await createComment(post.id, user.id, comment); setComment(""); onCommentCreated?.(); - fetchPosts(); + + // Get current post from store + const currentPost = useFeedsStore + .getState() + .posts.find((p) => p.id === post.id); + if (currentPost) { + useFeedsStore.getState().updatePost(post.id, { + comment_count: (currentPost.comment_count ?? 0) + 1, + }); + } } finally { setIsSubmitting(false); } diff --git a/apps/codebility/store/feeds-store.ts b/apps/codebility/store/feeds-store.ts index 249f920a3..084371564 100644 --- a/apps/codebility/store/feeds-store.ts +++ b/apps/codebility/store/feeds-store.ts @@ -3,20 +3,28 @@ import { getPosts, PostType } from "../app/home/feeds/_services/query"; interface FeedsState { posts: PostType[]; - isLoading: boolean; + isFetchingPosts: boolean; // only for feed fetch fetchPosts: () => Promise; + updatePost: (postId: string | number, data: Partial) => void; setPosts: (posts: PostType[]) => void; } export const useFeedsStore = create((set) => ({ posts: [], - isLoading: true, + isFetchingPosts: true, fetchPosts: async () => { - set({ isLoading: true }); + set({ isFetchingPosts: true }); const posts = await getPosts(); - set({ posts, isLoading: false }); + set({ posts, isFetchingPosts: false }); }, + updatePost: (postId, data) => + set((state) => ({ + posts: state.posts.map((p) => + p.id === postId ? { ...p, ...data } : p + ), + })), + setPosts: (posts) => set({ posts }), }));