Skip to content

Commit fb5c86e

Browse files
authored
Merge pull request #429 from CodebilityDev/dev
fix critical bug
2 parents 9379eae + 73ec409 commit fb5c86e

9 files changed

Lines changed: 395 additions & 73 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
44
import DefaultPagination from "@/components/ui/pagination";
55
import { useFeedsStore } from "@/store/feeds-store"; // Adjust path as needed
66

7-
import Post from "./Post";
7+
import Post from "./PostCard";
88

99
interface FeedProp {
1010
isAdmin: boolean;
File renamed without changes.

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import { useEffect, useState } from "react";
44
import { useUserStore } from "@/store/codev-store";
5+
import { useFeedsStore } from "@/store/feeds-store";
56
import { ArrowBigUp } from "lucide-react";
67

78
import {
89
AddPostUpvote,
10+
countUpvotes,
911
hasUserUpvoted,
1012
removePostUpvote,
1113
} from "../_services/action";
@@ -19,7 +21,11 @@ export default function PostUpvote({ post }: PostUpvoteProps) {
1921
const { user } = useUserStore();
2022

2123
const [isUpvoted, setIsUpvoted] = useState(false);
22-
const [upvotes, setUpvotes] = useState(post.upvoters_id?.length || 0);
24+
const [upvotes, setUpvotes] = useState(0);
25+
const { posts, fetchPosts } = useFeedsStore((state) => ({
26+
posts: state.posts,
27+
fetchPosts: state.fetchPosts,
28+
}));
2329

2430
useEffect(() => {
2531
const checkUpvote = async () => {
@@ -34,33 +40,43 @@ export default function PostUpvote({ post }: PostUpvoteProps) {
3440
} else {
3541
setIsUpvoted(false);
3642
}
37-
};
3843

44+
if (post?.id) {
45+
try {
46+
const upvotesCount = await countUpvotes(post.id);
47+
setUpvotes(upvotesCount);
48+
} catch (error) {
49+
console.error("Error counting upvotes:", error);
50+
}
51+
}
52+
};
3953
checkUpvote();
40-
}, [user?.id, post?.id]);
54+
}, [user?.id, post?.id, posts]);
4155

42-
const handleUpvote = (e: React.MouseEvent) => {
56+
const handleUpvote = async (e: React.MouseEvent) => {
4357
if (user) {
4458
e.stopPropagation();
4559
e.preventDefault();
4660

61+
setIsUpvoted((prev) => !prev);
62+
setUpvotes((prev) => prev + (isUpvoted ? -1 : 1));
63+
4764
if (!isUpvoted) {
48-
AddPostUpvote(post.id, user.id);
65+
const postUpvote = await AddPostUpvote(post.id, user.id);
4966
} else {
50-
removePostUpvote(post.id, user.id);
67+
await removePostUpvote(post.id, user.id);
5168
}
5269

53-
setIsUpvoted((prev) => !prev);
54-
setUpvotes((prev) => prev + (isUpvoted ? -1 : 1));
70+
await fetchPosts();
5571
}
5672
};
5773

5874
return (
5975
<button
6076
className={`flex items-center space-x-1 ${
6177
isUpvoted
62-
? "text-customBlue-500 dark:text-customBlue-400"
63-
: "hover:text-customBlue-500 dark:hover:text-customBlue-400"
78+
? "text-customBlue-300 dark:text-customBlue-200"
79+
: "hover:text-customBlue-300 dark:hover:text-customBlue-200"
6480
}`}
6581
onClick={handleUpvote}
6682
>

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import remarkGfm from "remark-gfm";
1717
import type { PostType } from "../_services/query";
1818
import PostUpvote from "../_components/PostUpvote";
1919
import { getUserRole } from "../_services/action";
20+
import PostViewCommentList from "./PostViewCommentList";
21+
import PostViewCreateComment from "./PostViewCreateComment";
2022

2123
interface PostViewProps {
2224
postId: string;
@@ -36,6 +38,11 @@ export default function PostView({ postId }: PostViewProps) {
3638
const [isAdmin, setIsAdmin] = useState(true);
3739
const [isAuthor, setIsAuthor] = useState(false);
3840
const { user } = useUserStore();
41+
const [refreshComments, setRefreshComments] = useState(0);
42+
43+
const triggerRefreshComments = () => {
44+
setRefreshComments((prev) => prev + 1);
45+
};
3946

4047
useEffect(() => {
4148
const fetchRole = async () => {
@@ -136,17 +143,31 @@ export default function PostView({ postId }: PostViewProps) {
136143

137144
<PostUpvote post={post} />
138145

139-
<div className="prose prose-sm dark:prose-invert max-h-[800px] min-h-[160px] max-w-none overflow-y-auto p-4 text-sm">
140-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
141-
{`![Post image](${post.image_url || "/assets/images/bg-certificate.png"})\n\n${post.content}`}
142-
</ReactMarkdown>
146+
{/* Scrollable group */}
147+
<div className="max-h-[90vh] overflow-y-auto">
148+
<div className="prose prose-sm dark:prose-invert max-w-none text-sm">
149+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
150+
{`![Post image](${post.image_url || "/assets/images/bg-certificate.png"})\n\n${post.content}`}
151+
</ReactMarkdown>
152+
</div>
153+
154+
{(isAdmin || isAuthor) && (
155+
<Button className="mb-4 mt-4 w-auto" onClick={openModal}>
156+
Edit Post
157+
</Button>
158+
)}
159+
<div className="mb-1 mt-1 border-t border-gray-200 dark:border-gray-700" />
160+
<PostViewCommentList
161+
postId={postId}
162+
refresh={refreshComments}
163+
hasDeleteCommentPrivilege={isAdmin || isAuthor}
164+
/>
143165
</div>
166+
<PostViewCreateComment
167+
postId={postId}
168+
onCommentCreated={triggerRefreshComments}
169+
/>
144170

145-
{(isAdmin || isAuthor) && (
146-
<Button className="mb-4 mt-4 w-auto" onClick={openModal}>
147-
Edit Post
148-
</Button>
149-
)}
150171
<EditPostModal
151172
post={post}
152173
isOpen={isModalOpen}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Image from "next/image";
5+
import { X } from "lucide-react";
6+
7+
import { deletePostComment } from "../_services/action";
8+
9+
interface PostViewCommentItemProps {
10+
commenntId: string;
11+
userImage: string;
12+
userName: string;
13+
content: string;
14+
userCanDelete: boolean;
15+
}
16+
17+
export default function PostViewCommentItem({
18+
commenntId,
19+
userImage,
20+
userName,
21+
content,
22+
userCanDelete,
23+
}: PostViewCommentItemProps) {
24+
const [isVisible, setIsVisible] = useState(true);
25+
26+
const handleDelete = async () => {
27+
setIsVisible(false);
28+
try {
29+
await deletePostComment(commenntId);
30+
} catch (error) {
31+
setIsVisible(true);
32+
// Optionally, you could log the error or show a message to the user
33+
}
34+
};
35+
36+
if (!isVisible) return null;
37+
38+
return (
39+
<div className="group relative flex items-start gap-3 rounded-md p-2 transition hover:bg-gray-50 dark:hover:bg-gray-800/40">
40+
{/* Avatar */}
41+
<Image
42+
src={
43+
userImage ||
44+
"https://codebility-cdn.pages.dev/assets/images/default-avatar-200x200.jpg"
45+
}
46+
alt={`${userName}'s avatar`}
47+
width={36}
48+
height={36}
49+
className="h-9 w-9 rounded-full object-cover"
50+
/>
51+
52+
{/* User content */}
53+
<div className="flex flex-col">
54+
<span className="text-sm font-semibold text-gray-800 dark:text-gray-100">
55+
{userName}
56+
</span>
57+
<p className="break-words text-sm text-gray-700 dark:text-gray-300">
58+
{content}
59+
</p>
60+
</div>
61+
62+
{/* X Icon (only visible on hover) */}
63+
{userCanDelete && (
64+
<button
65+
onClick={handleDelete}
66+
className="absolute right-2 top-2 rounded-full p-1 opacity-0 transition-opacity hover:bg-gray-200 group-hover:opacity-100 dark:hover:bg-gray-700"
67+
aria-label="Delete comment"
68+
>
69+
<X size={16} className="text-gray-600 dark:text-gray-300" />
70+
</button>
71+
)}
72+
</div>
73+
);
74+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import { defaultAvatar } from "@/public/assets/images";
5+
import { useUserStore } from "@/store/codev-store";
6+
7+
import { getPostComments } from "../_services/action";
8+
import PostViewCommentItem from "./PostViewCommentItem";
9+
10+
interface PostViewCommentListProps {
11+
postId: string;
12+
refresh: number;
13+
hasDeleteCommentPrivilege: boolean;
14+
}
15+
16+
interface Comment {
17+
id: string;
18+
content: string;
19+
created_at: string;
20+
commenter: any;
21+
}
22+
23+
export default function PostViewCommentList({
24+
postId,
25+
refresh,
26+
hasDeleteCommentPrivilege,
27+
}: PostViewCommentListProps) {
28+
const [comments, setComments] = useState<Comment[]>([]);
29+
const [loading, setLoading] = useState(true);
30+
const { user } = useUserStore();
31+
32+
useEffect(() => {
33+
const fetchComments = async () => {
34+
try {
35+
const data = await getPostComments(postId);
36+
setComments(data);
37+
} catch (error) {
38+
console.error("Error fetching comments:", error);
39+
} finally {
40+
setLoading(false);
41+
}
42+
};
43+
44+
fetchComments();
45+
}, [postId, refresh]);
46+
47+
if (loading) {
48+
return (
49+
<p className="text-sm text-gray-500 dark:text-gray-400">
50+
Loading comments…
51+
</p>
52+
);
53+
}
54+
55+
if (comments.length === 0) {
56+
return (
57+
<p className="text-sm text-gray-500 dark:text-gray-400">
58+
No comments yet.
59+
</p>
60+
);
61+
}
62+
63+
return (
64+
<div className="flex flex-col gap-2">
65+
{comments.map((c) => (
66+
<PostViewCommentItem
67+
key={c.id}
68+
commenntId={c.id}
69+
userImage={c.commenter?.image_url || defaultAvatar}
70+
userName={`${c.commenter?.first_name} ${c.commenter?.last_name}`}
71+
content={c.content}
72+
userCanDelete={
73+
user?.id == c.commenter?.id || hasDeleteCommentPrivilege
74+
}
75+
/>
76+
))}
77+
</div>
78+
);
79+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import Image from "next/image";
5+
import { defaultAvatar } from "@/public/assets/images";
6+
import { useUserStore } from "@/store/codev-store";
7+
8+
import { createComment } from "../_services/action";
9+
10+
interface CreateCommentProps {
11+
postId: string;
12+
onCommentCreated?: () => void;
13+
}
14+
15+
export default function PostViewCreateComment({
16+
postId,
17+
onCommentCreated,
18+
}: CreateCommentProps) {
19+
const [comment, setComment] = useState("");
20+
const [isSubmitting, setIsSubmitting] = useState(false);
21+
const { user } = useUserStore();
22+
23+
const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
24+
if (e.key === "Enter" && comment.trim() && !isSubmitting) {
25+
if (!user || !user.id) return;
26+
try {
27+
setIsSubmitting(true);
28+
await createComment(postId, user.id, comment);
29+
setComment("");
30+
onCommentCreated?.();
31+
} finally {
32+
setIsSubmitting(false);
33+
}
34+
}
35+
};
36+
37+
return (
38+
<div className="flex items-center gap-3 p-2">
39+
<Image
40+
src={user?.image_url || defaultAvatar}
41+
alt="User avatar"
42+
width={36}
43+
height={36}
44+
className="h-9 w-9 rounded-full object-cover"
45+
/>
46+
<input
47+
type="text"
48+
placeholder="Write a comment..."
49+
value={comment}
50+
onChange={(e) => setComment(e.target.value)}
51+
onKeyDown={handleKeyDown}
52+
disabled={isSubmitting}
53+
className={`flex-1 rounded-full border border-gray-300 bg-white px-4 py-2 text-sm outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white ${
54+
isSubmitting
55+
? "cursor-not-allowed opacity-50"
56+
: "focus:border-gray-500"
57+
}`}
58+
/>
59+
</div>
60+
);
61+
}

0 commit comments

Comments
 (0)