Skip to content

Commit bcfc877

Browse files
committed
Likes for coments and posts, responsive ui edit
1 parent 50dd13c commit bcfc877

4 files changed

Lines changed: 479 additions & 169 deletions

File tree

‎apps/codebility/app/home/overflow/_components/CommentSection.tsx‎

Lines changed: 93 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ import {
2121
DialogTitle,
2222
} from "@/components/ui/dialog";
2323
import { Heart, Send, Clock, MoreVertical, Pencil, Trash2, X, Check } from "lucide-react";
24-
import { fetchComments, postComment, toggleCommentLike, updateComment, deleteComment, type Comment } from "../actions";
24+
import {
25+
fetchComments,
26+
postComment,
27+
toggleCommentLike,
28+
updateComment,
29+
deleteComment,
30+
fetchCommentLikes,
31+
type Comment
32+
} from "../actions";
2533
import { Question, fetchQuestions } from "../actions";
2634

2735
interface CommentSectionProps {
@@ -95,86 +103,87 @@ const CommentItem = memo(
95103

96104
const handleDelete = () => {
97105
onDelete(comment.id);
98-
// Use setTimeout to ensure modal opens before dropdown closes
99106
setTimeout(() => {
100107
setDropdownOpen(false);
101108
}, 0);
102109
};
103110

104111
return (
105-
<div className="flex gap-3">
106-
<div className="h-10 w-10 mt-3 flex-shrink-0 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
112+
<div className="flex gap-2 sm:gap-3">
113+
{/* Avatar - Responsive sizing */}
114+
<div className="h-8 w-8 sm:h-10 sm:w-10 mt-2 sm:mt-3 flex-shrink-0 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
107115
{comment.author.image_url ? (
108116
<Image
109117
src={comment.author.image_url}
110118
alt={comment.author.name}
111-
width={32}
112-
height={32}
119+
width={40}
120+
height={40}
113121
className="h-full w-full object-cover"
114122
/>
115123
) : (
116124
<DefaultAvatar size={32} />
117125
)}
118126
</div>
119127

120-
<div className="flex-1 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
121-
<div className="mb-2 flex items-start justify-between">
122-
<div className="flex items-center gap-2">
123-
<div>
124-
<span className="text-sm font-medium text-gray-900 dark:text-white">
125-
{comment.author.name}
126-
</span>
127-
<div className="flex items-center gap-1 text-xs text-gray-600 dark:text-white">
128-
<Clock className="h-3 w-3" />
129-
{comment.created_at === comment.updated_at ? (
130-
<TimeAgo date={comment.created_at} />
131-
) : (
132-
<span className="flex items-center gap-1">
133-
<span>edited</span>
134-
<TimeAgo date={comment.updated_at} />
135-
</span>
136-
)}
137-
</div>
128+
{/* Comment content */}
129+
<div className="flex-1 min-w-0 rounded-lg border border-gray-200 bg-gray-50 p-2.5 sm:p-3 dark:border-gray-700 dark:bg-gray-800">
130+
<div className="mb-2 flex items-start justify-between gap-2">
131+
<div className="flex-1 min-w-0">
132+
<span className="text-xs sm:text-sm font-medium text-gray-900 dark:text-white block truncate">
133+
{comment.author.name}
134+
</span>
135+
<div className="flex items-center gap-1 text-xs text-gray-600 dark:text-white">
136+
<Clock className="h-3 w-3 flex-shrink-0" />
137+
{comment.created_at === comment.updated_at ? (
138+
<TimeAgo date={comment.created_at} />
139+
) : (
140+
<span className="flex items-center gap-1">
141+
<span>edited</span>
142+
<TimeAgo date={comment.updated_at} />
143+
</span>
144+
)}
138145
</div>
139146
</div>
140-
<div className="flex items-center gap-1">
147+
148+
{/* Action buttons - Responsive */}
149+
<div className="flex items-center gap-0.5 sm:gap-1 flex-shrink-0">
141150
<Button
142151
variant="ghost"
143152
size="sm"
144153
onClick={() => onLike(comment.id)}
145-
className={`flex items-center gap-1 px-2 py-1 ${
154+
className={`flex items-center gap-1 px-1.5 sm:px-2 py-1 h-auto ${
146155
isLiked
147156
? "text-red-600 hover:text-red-700"
148157
: "text-gray-600 hover:text-gray-700 dark:text-white dark:hover:text-gray-200"
149158
}`}
150159
>
151-
<Heart className={`h-5 w-5 ${isLiked ? "fill-current" : ""}`} />
152-
<span className="text-sm">{comment.likes}</span>
160+
<Heart className={`h-4 w-4 sm:h-5 sm:w-5 ${isLiked ? "fill-current" : ""}`} />
161+
<span className="text-xs sm:text-sm">{comment.likes}</span>
153162
</Button>
154163

155164
{!isEditing && (
156165
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
157166
<DropdownMenuTrigger asChild>
158167
<Button
159-
disabled={!isOwner}
160168
variant="ghost"
169+
disabled={!isOwner}
161170
size="sm"
162-
className="px-2 py-1 text-gray-600 hover:text-gray-700 dark:text-white dark:hover:text-gray-200"
171+
className="px-1.5 sm:px-2 py-1 h-auto text-gray-600 hover:text-gray-700 dark:text-white dark:hover:text-gray-200"
163172
>
164-
<MoreVertical className="h-5 w-5" />
173+
<MoreVertical className="h-4 w-4 sm:h-5 sm:w-5" />
165174
</Button>
166175
</DropdownMenuTrigger>
167-
<DropdownMenuContent align="end">
168-
<DropdownMenuItem onClick={handleStartEdit}>
169-
<Pencil className="mr-2 h-4 w-4" />
176+
<DropdownMenuContent align="end" className="w-32 sm:w-auto">
177+
<DropdownMenuItem onClick={handleStartEdit} className="text-xs sm:text-sm">
178+
<Pencil className="mr-2 h-3 w-3 sm:h-4 sm:w-4" />
170179
Edit
171180
</DropdownMenuItem>
172181
<DropdownMenuItem
173182
onClick={handleDelete}
174183
onSelect={(e) => e.preventDefault()}
175-
className="text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
184+
className="text-xs sm:text-sm text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
176185
>
177-
<Trash2 className="mr-2 h-4 w-4" />
186+
<Trash2 className="mr-2 h-3 w-3 sm:h-4 sm:w-4" />
178187
Delete
179188
</DropdownMenuItem>
180189
</DropdownMenuContent>
@@ -189,31 +198,32 @@ const CommentItem = memo(
189198
value={editContent}
190199
onChange={(e) => setEditContent(e.target.value)}
191200
rows={3}
192-
className="resize-none border-gray-300 bg-white text-gray-900 placeholder-gray-500 focus:border-customBlue-500 focus:ring-customBlue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-500 dark:focus:border-customBlue-400 dark:focus:ring-customBlue-400"
201+
className="resize-none border-gray-300 bg-white text-sm sm:text-base text-gray-900 placeholder-gray-500 focus:border-customBlue-500 focus:ring-customBlue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-500 dark:focus:border-customBlue-400 dark:focus:ring-customBlue-400"
193202
autoFocus
194203
/>
195204
<div className="flex justify-end gap-2">
196205
<Button
197206
variant="ghost"
198207
size="sm"
199208
onClick={handleCancelEdit}
200-
className="text-gray-600 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200"
209+
className="text-xs sm:text-sm h-8 sm:h-9 text-gray-600 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200"
201210
>
202-
<X className="mr-1 h-4 w-4" />
211+
<X className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
203212
Cancel
204213
</Button>
205214
<Button
206215
size="sm"
207216
onClick={handleSaveEdit}
208217
disabled={!editContent.trim() || editContent === comment.content}
218+
className="text-xs sm:text-sm h-8 sm:h-9"
209219
>
210-
<Check className="mr-1 h-4 w-4" />
220+
<Check className="mr-1 h-3 w-3 sm:h-4 sm:w-4" />
211221
Save
212222
</Button>
213223
</div>
214224
</div>
215225
) : (
216-
<p className="text-sm leading-relaxed text-gray-700 dark:text-white">
226+
<p className="text-xs sm:text-sm leading-relaxed text-gray-700 dark:text-white break-words">
217227
{comment.content}
218228
</p>
219229
)}
@@ -240,8 +250,13 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
240250
const loadComments = async () => {
241251
setIsLoading(true);
242252
try {
243-
const fetchedComments = await fetchComments(questionId);
253+
const [fetchedComments, likedCommentIds] = await Promise.all([
254+
fetchComments(questionId),
255+
fetchCommentLikes(loggedIn.id, questionId)
256+
]);
257+
244258
setComments(fetchedComments);
259+
setLikedComments(new Set(likedCommentIds));
245260
} catch (error) {
246261
console.error("Error loading comments:", error);
247262
} finally {
@@ -289,6 +304,7 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
289304
const wasLiked = likedComments.has(commentId);
290305
const newLikedComments = new Set(likedComments);
291306

307+
// Optimistic update
292308
if (wasLiked) {
293309
newLikedComments.delete(commentId);
294310
setComments((prev) =>
@@ -301,12 +317,13 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
301317
);
302318
}
303319

304-
setLikedComments(new Set(newLikedComments));
320+
setLikedComments(newLikedComments);
305321

306322
try {
307-
const result = await toggleCommentLike(commentId, !wasLiked);
323+
const result = await toggleCommentLike(commentId, loggedIn.id);
308324

309325
if (!result.success) {
326+
// Revert on failure
310327
if (wasLiked) {
311328
newLikedComments.add(commentId);
312329
setComments((prev) =>
@@ -318,22 +335,34 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
318335
prev.map((c) => (c.id === commentId ? { ...c, likes: c.likes - 1 } : c))
319336
);
320337
}
321-
setLikedComments(new Set(newLikedComments));
338+
setLikedComments(newLikedComments);
339+
console.error("Failed to toggle like:", result.error);
322340
}
323341
} catch (error) {
342+
// Revert on error
343+
if (wasLiked) {
344+
newLikedComments.add(commentId);
345+
setComments((prev) =>
346+
prev.map((c) => (c.id === commentId ? { ...c, likes: c.likes + 1 } : c))
347+
);
348+
} else {
349+
newLikedComments.delete(commentId);
350+
setComments((prev) =>
351+
prev.map((c) => (c.id === commentId ? { ...c, likes: c.likes - 1 } : c))
352+
);
353+
}
354+
setLikedComments(newLikedComments);
324355
console.error("Error toggling like:", error);
325356
}
326357
};
327358

328359
const handleEditComment = async (commentId: string, newContent: string) => {
329-
// Store original comment for potential revert
330360
const originalComment = comments.find(c => c.id === commentId);
331361
if (!originalComment) return;
332362

333363
const originalContent = originalComment.content;
334364
const originalUpdatedAt = originalComment.updated_at;
335365

336-
// Optimistically update the UI with current timestamp
337366
const newUpdatedAt = new Date().toISOString();
338367
setComments(prev =>
339368
prev.map(c => c.id === commentId
@@ -349,7 +378,6 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
349378
});
350379

351380
if (!result.success) {
352-
// Revert on failure
353381
setComments(prev =>
354382
prev.map(c => c.id === commentId
355383
? { ...c, content: originalContent, updated_at: originalUpdatedAt }
@@ -358,14 +386,12 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
358386
);
359387
alert("Failed to update comment. Please try again.");
360388
} else if (result.comment) {
361-
// Update with actual server response
362389
setComments(prev =>
363390
prev.map(c => c.id === commentId ? result.comment! : c)
364391
);
365392
}
366393
} catch (error) {
367394
console.error("Error editing comment:", error);
368-
// Revert on error
369395
setComments(prev =>
370396
prev.map(c => c.id === commentId
371397
? { ...c, content: originalContent, updated_at: originalUpdatedAt }
@@ -390,10 +416,8 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
390416
const result = await deleteComment(commentToDelete);
391417

392418
if (result.success) {
393-
// Remove comment from UI
394419
setComments(prev => prev.filter(c => c.id !== commentToDelete));
395420

396-
// Update question count
397421
fetchQuestions()
398422
.then((result) => {
399423
setQuestions(result);
@@ -416,13 +440,13 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
416440
};
417441

418442
const cancelDelete = () => {
419-
if (isDeleting) return; // Prevent closing while deleting
443+
if (isDeleting) return;
420444
setDeleteModalOpen(false);
421445
setCommentToDelete(null);
422446
};
423447

424448
const handleDialogChange = (open: boolean) => {
425-
if (!open && isDeleting) return; // Prevent closing while deleting
449+
if (!open && isDeleting) return;
426450
setDeleteModalOpen(open);
427451
if (!open) {
428452
setCommentToDelete(null);
@@ -433,36 +457,37 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
433457
return (
434458
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-center dark:border-gray-700 dark:bg-gray-800">
435459
<div className="mb-3 flex justify-center">
436-
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 border-t-customBlue-500 dark:border-gray-700 dark:border-t-customBlue-400"></div>
460+
<div className="h-6 w-6 sm:h-8 sm:w-8 animate-spin rounded-full border-2 border-gray-200 border-t-customBlue-500 dark:border-gray-700 dark:border-t-customBlue-400"></div>
437461
</div>
438-
<p className="text-sm text-gray-600 dark:text-white">Loading comments...</p>
462+
<p className="text-xs sm:text-sm text-gray-600 dark:text-white">Loading comments...</p>
439463
</div>
440464
);
441465
}
442466

443467
return (
444-
<div className="space-y-4">
468+
<div className="space-y-3 sm:space-y-4">
445469
{/* Delete Confirmation Modal */}
446470
<Dialog open={deleteModalOpen} onOpenChange={handleDialogChange}>
447-
<DialogContent className="sm:max-w-[425px]">
471+
<DialogContent className="w-[90vw] max-w-[425px] rounded-lg">
448472
<DialogHeader>
449-
<DialogTitle>Delete Comment</DialogTitle>
450-
<DialogDescription>
473+
<DialogTitle className="text-base sm:text-lg">Delete Comment</DialogTitle>
474+
<DialogDescription className="text-xs sm:text-sm">
451475
Are you sure you want to delete this comment? This action cannot be undone.
452476
</DialogDescription>
453477
</DialogHeader>
454-
<DialogFooter>
478+
<DialogFooter className="gap-2 sm:gap-0">
455479
<Button
456480
variant="ghost"
457481
onClick={cancelDelete}
458482
disabled={isDeleting}
483+
className="text-xs sm:text-sm h-9"
459484
>
460485
Cancel
461486
</Button>
462487
<Button
463488
onClick={confirmDelete}
464489
disabled={isDeleting}
465-
className="bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600"
490+
className="text-xs sm:text-sm h-9 bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600"
466491
>
467492
{isDeleting ? "Deleting..." : "Delete"}
468493
</Button>
@@ -472,7 +497,7 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
472497

473498
{/* Comments List */}
474499
{comments.length > 0 && (
475-
<div className="space-y-3">
500+
<div className="space-y-2.5 sm:space-y-3">
476501
{comments.map((comment) => (
477502
<CommentItem
478503
key={comment.id}
@@ -488,31 +513,32 @@ export default function CommentSection({ questionId, loggedIn, setQuestions }: C
488513
)}
489514

490515
{/* Add Comment Form */}
491-
<form onSubmit={handleSubmitComment} className="space-y-3">
516+
<form onSubmit={handleSubmitComment} className="space-y-2 sm:space-y-3">
492517
<Textarea
493518
placeholder="Write a helpful comment..."
494519
value={newComment}
495520
onChange={(e) => setNewComment(e.target.value)}
496521
disabled={isSubmitting}
497522
rows={3}
498-
className="resize-none border-gray-300 bg-gray-50 text-gray-900 placeholder-gray-500 focus:border-customBlue-500 focus:ring-customBlue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500 dark:focus:border-customBlue-400 dark:focus:ring-customBlue-400"
523+
className="resize-none text-sm sm:text-base border-gray-300 bg-gray-50 text-gray-900 placeholder-gray-500 focus:border-customBlue-500 focus:ring-customBlue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500 dark:focus:border-customBlue-400 dark:focus:ring-customBlue-400"
499524
/>
500525

501526
<div className="flex justify-end">
502527
<Button
503528
type="submit"
504529
size="sm"
505530
disabled={isSubmitting || !newComment.trim()}
531+
className="text-xs sm:text-sm h-9"
506532
>
507-
<Send className="mr-2 h-5 w-5" />
533+
<Send className="mr-1.5 sm:mr-2 h-4 w-4" />
508534
{isSubmitting ? "Posting..." : "Post Comment"}
509535
</Button>
510536
</div>
511537
</form>
512538

513539
{comments.length === 0 && (
514540
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 text-center dark:border-gray-700 dark:bg-gray-800">
515-
<p className="text-sm text-gray-600 dark:text-white">
541+
<p className="text-xs sm:text-sm text-gray-600 dark:text-white">
516542
No comments yet. Be the first to help!
517543
</p>
518544
</div>

0 commit comments

Comments
 (0)