Skip to content

Commit aa71b13

Browse files
authored
Merge pull request #444 from CodebilityDev/miguel/feeds-improvement
Miguel/feeds improvement
2 parents 0a24de1 + 19a6cae commit aa71b13

7 files changed

Lines changed: 231 additions & 64 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogAction,
4+
AlertDialogCancel,
5+
AlertDialogContent,
6+
AlertDialogDescription,
7+
AlertDialogFooter,
8+
AlertDialogHeader,
9+
AlertDialogTitle,
10+
} from "@codevs/ui/alert-dialog";
11+
12+
interface DeleteDialogProps {
13+
isOpen: boolean;
14+
onClose: () => void;
15+
onConfirm: () => void;
16+
isLoading?: boolean;
17+
}
18+
19+
export function DeleteDialog({
20+
isOpen,
21+
onClose,
22+
onConfirm,
23+
isLoading,
24+
}: DeleteDialogProps) {
25+
return (
26+
<AlertDialog open={isOpen} onOpenChange={onClose}>
27+
<AlertDialogContent className="bg-light-300 dark:bg-dark-100">
28+
<AlertDialogHeader>
29+
<AlertDialogTitle className="dark:text-light-900 text-black">
30+
Delete Post
31+
</AlertDialogTitle>
32+
<AlertDialogDescription className="text-gray dark:text-light-500">
33+
Are you sure you want to delete this post? This action cannot be
34+
undone.
35+
</AlertDialogDescription>
36+
</AlertDialogHeader>
37+
38+
<AlertDialogFooter className="mt-6">
39+
<AlertDialogCancel
40+
disabled={isLoading}
41+
className="border-light-700 dark:border-dark-200
42+
bg-light-800 dark:bg-dark-200 dark:text-light-900 text-black"
43+
>
44+
Cancel
45+
</AlertDialogCancel>
46+
47+
<AlertDialogAction
48+
onClick={onConfirm}
49+
disabled={isLoading}
50+
className="bg-red-500 hover:bg-red-600 focus:ring-red-500"
51+
>
52+
{isLoading ? "Deleting..." : "Delete"}
53+
</AlertDialogAction>
54+
</AlertDialogFooter>
55+
</AlertDialogContent>
56+
</AlertDialog>
57+
);
58+
}

‎apps/codebility/app/home/feeds/_components/Feed.tsx‎

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,57 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { useEffect, useMemo, useState } from "react";
44
import DefaultPagination from "@/components/ui/pagination";
5-
import { useFeedsStore } from "@/store/feeds-store"; // Adjust path as needed
5+
import { useFeedsStore } from "@/store/feeds-store";
66

77
import Post from "./PostCard";
88

99
interface FeedProp {
1010
isAdmin: boolean;
11-
onCreatePost?: () => void;
11+
searchQuery?: string;
1212
}
1313

14-
export default function Feed({ isAdmin }: FeedProp) {
14+
export default function Feed({ isAdmin, searchQuery }: FeedProp) {
1515
const posts = useFeedsStore((state) => state.posts);
1616
const fetchPosts = useFeedsStore((state) => state.fetchPosts);
1717

1818
const [currentPage, setCurrentPage] = useState(1);
1919

20+
useEffect(() => {
21+
fetchPosts();
22+
}, [fetchPosts]);
23+
24+
// Filter posts by title or author's first/last name (case-insensitive)
25+
const filteredPosts = useMemo(() => {
26+
if (!searchQuery || searchQuery.trim() === "") return posts;
27+
28+
const query = searchQuery.toLowerCase();
29+
30+
return posts.filter((post) => {
31+
const titleMatch = post.title?.toLowerCase().includes(query);
32+
const firstNameMatch = post.author_id?.first_name
33+
?.toLowerCase()
34+
.includes(query);
35+
const lastNameMatch = post.author_id?.last_name
36+
?.toLowerCase()
37+
.includes(query);
38+
39+
return titleMatch || firstNameMatch || lastNameMatch;
40+
});
41+
}, [posts, searchQuery]);
42+
2043
const postsPerPage = 6;
21-
const totalPages = Math.ceil(posts.length / postsPerPage);
44+
const totalPages = Math.ceil(filteredPosts.length / postsPerPage);
2245
const startIndex = (currentPage - 1) * postsPerPage;
23-
const currentPosts = posts.slice(startIndex, startIndex + postsPerPage);
46+
const currentPosts = filteredPosts.slice(
47+
startIndex,
48+
startIndex + postsPerPage,
49+
);
2450

51+
// Reset to first page when search changes
2552
useEffect(() => {
26-
fetchPosts();
27-
}, [fetchPosts]);
53+
setCurrentPage(1);
54+
}, [searchQuery]);
2855

2956
const handleDeletePost = (deletedPostId: string | number) => {
3057
useFeedsStore.setState((state) => ({
@@ -52,13 +79,16 @@ export default function Feed({ isAdmin }: FeedProp) {
5279
/>
5380
))}
5481
</div>
55-
<DefaultPagination
56-
totalPages={totalPages}
57-
currentPage={currentPage}
58-
handleNextPage={handleNextPage}
59-
handlePreviousPage={handlePreviousPage}
60-
setCurrentPage={setCurrentPage}
61-
/>
82+
83+
{filteredPosts.length > 0 && (
84+
<DefaultPagination
85+
totalPages={totalPages}
86+
currentPage={currentPage}
87+
handleNextPage={handleNextPage}
88+
handlePreviousPage={handlePreviousPage}
89+
setCurrentPage={setCurrentPage}
90+
/>
91+
)}
6292
</>
6393
);
6494
}

‎apps/codebility/app/home/feeds/_components/PostCard.tsx‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import toast from "react-hot-toast";
1111

1212
import { deletePost } from "../_services/action";
1313
import { PostType } from "../_services/query";
14+
import { DeleteDialog } from "./DeleteDialog";
1415
import PostUpvote from "./PostUpvote";
1516

1617
interface PostProps {
@@ -22,6 +23,8 @@ interface PostProps {
2223
export default function Post({ post, isAdmin, onDelete }: PostProps) {
2324
const [isModalOpen, setIsModalOpen] = useState(false);
2425
const [isAuthor, setIsAuthor] = useState(false);
26+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
27+
const [isDeleting, setIsDeleting] = useState(false);
2528
const { user } = useUserStore();
2629

2730
useEffect(() => {
@@ -38,20 +41,26 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
3841
checkIfAuthor();
3942
}
4043
}, [user]);
41-
42-
const handleDelete = async (e: React.MouseEvent) => {
44+
const openDeleteDialog = async (e: React.MouseEvent) => {
4345
e.stopPropagation();
4446
e.preventDefault();
47+
setShowDeleteDialog(true);
48+
};
49+
50+
const handleDelete = async () => {
4551
try {
52+
setIsDeleting(true);
4653
await deletePost(post.id);
47-
toast.success("Post deleted successfully!");
48-
4954
if (onDelete) {
5055
onDelete(post.id);
5156
}
57+
toast.success("Post deleted successfully!");
5258
} catch (error) {
5359
toast.error("Failed to delete post.");
5460
console.error("Failed to delete post:", error);
61+
} finally {
62+
setIsDeleting(false);
63+
setShowDeleteDialog(false);
5564
}
5665
};
5766

@@ -67,7 +76,7 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
6776
{(isAdmin || isAuthor) && (
6877
<button
6978
className="invisible absolute right-2 top-2 rounded bg-red-500 px-2 py-1 text-white hover:bg-red-600 group-hover:visible"
70-
onClick={handleDelete}
79+
onClick={openDeleteDialog}
7180
aria-label="Delete post"
7281
>
7382
✕
@@ -122,6 +131,12 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
122131
onClose={closeModal}
123132
postId={post.id}
124133
/>
134+
<DeleteDialog
135+
isOpen={showDeleteDialog}
136+
onClose={() => setShowDeleteDialog(false)}
137+
onConfirm={handleDelete}
138+
isLoading={isDeleting}
139+
/>
125140
</div>
126141
);
127142
}

‎apps/codebility/app/home/feeds/_components/PostViewCommentItem.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState } from "react";
44
import Image from "next/image";
5+
import { useFeedsStore } from "@/store/feeds-store";
56
import { X } from "lucide-react";
67

78
import { deletePostComment } from "../_services/action";
@@ -21,15 +22,16 @@ export default function PostViewCommentItem({
2122
content,
2223
userCanDelete,
2324
}: PostViewCommentItemProps) {
25+
const fetchPosts = useFeedsStore((state) => state.fetchPosts);
2426
const [isVisible, setIsVisible] = useState(true);
2527

2628
const handleDelete = async () => {
2729
setIsVisible(false);
2830
try {
2931
await deletePostComment(commenntId);
32+
fetchPosts();
3033
} catch (error) {
3134
setIsVisible(true);
32-
// Optionally, you could log the error or show a message to the user
3335
}
3436
};
3537

‎apps/codebility/app/home/feeds/_components/PostViewCreateComment.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState } from "react";
44
import Image from "next/image";
55
import { defaultAvatar } from "@/public/assets/images";
66
import { useUserStore } from "@/store/codev-store";
7+
import { useFeedsStore } from "@/store/feeds-store";
78

89
import { createComment } from "../_services/action";
910

@@ -18,6 +19,7 @@ export default function PostViewCreateComment({
1819
}: CreateCommentProps) {
1920
const [comment, setComment] = useState("");
2021
const [isSubmitting, setIsSubmitting] = useState(false);
22+
const fetchPosts = useFeedsStore((state) => state.fetchPosts);
2123
const { user } = useUserStore();
2224

2325
const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -28,6 +30,7 @@ export default function PostViewCreateComment({
2830
await createComment(postId, user.id, comment);
2931
setComment("");
3032
onCommentCreated?.();
33+
fetchPosts();
3134
} finally {
3235
setIsSubmitting(false);
3336
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { useUserStore } from "@/store/codev-store";
5+
import { useFeedsStore } from "@/store/feeds-store";
6+
import { MessageSquareText } from "lucide-react";
7+
import { shallow } from "zustand/shallow";
8+
9+
import { getSocialPoints } from "../_services/action";
10+
11+
export default function SocialPointsCard() {
12+
const posts = useFeedsStore((state) => state.posts, shallow);
13+
const { user } = useUserStore();
14+
const [points, setPoints] = useState<number | null>(null);
15+
const [loading, setLoading] = useState(true);
16+
17+
useEffect(() => {
18+
const fetchSocialPoints = async () => {
19+
if (!user) {
20+
setLoading(false);
21+
return;
22+
}
23+
setLoading(true);
24+
try {
25+
const fetchedPoints = await getSocialPoints(user.id);
26+
setPoints(fetchedPoints || 0);
27+
} finally {
28+
setLoading(false);
29+
}
30+
};
31+
32+
if (user) fetchSocialPoints();
33+
}, [user, posts]);
34+
35+
return (
36+
<div className="dark:bg-white/3 relative mt-4 w-full rounded-xl border border-white/10 bg-gray-800 p-4 text-white shadow-lg backdrop-blur-sm transition-all duration-300 hover:scale-[1.02] hover:bg-white/10 hover:shadow-lg dark:border-white/5 xl:fixed xl:right-6 xl:top-20 xl:z-50 xl:mt-0 xl:w-64">
37+
<div className="relative">
38+
<div className="mb-3 flex items-center justify-between">
39+
<div className="flex items-center gap-2">
40+
<div className="rounded-lg bg-gradient-to-br from-blue-500 to-cyan-500 p-2 text-white">
41+
<MessageSquareText className="h-6 w-6" />
42+
</div>
43+
<div>
44+
<p className="text-sm font-medium text-gray-300">Social Points</p>
45+
<p className="text-xs text-gray-500">Feed Engagement</p>
46+
</div>
47+
</div>
48+
49+
<div className="text-right">
50+
{loading ? (
51+
<div className="flex justify-end">
52+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-cyan-500 border-t-transparent" />
53+
</div>
54+
) : (
55+
<>
56+
<p className="bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-2xl font-bold text-transparent">
57+
{points ?? 0}
58+
</p>
59+
<p className="text-xs text-gray-500">points</p>
60+
</>
61+
)}
62+
</div>
63+
</div>
64+
65+
<div className="space-y-1">
66+
<p className="text-xs text-gray-500 dark:text-gray-400">
67+
Earn more points by posting and getting likes and comments!
68+
</p>
69+
</div>
70+
</div>
71+
</div>
72+
);
73+
}

0 commit comments

Comments
 (0)