| 1 | import { type Component, createSignal, For, Show } from "solid-js";
|
|---|
| 2 | import { formatDateTime } from "@/utils";
|
|---|
| 3 | import { useAuth } from "@/context/AuthContext";
|
|---|
| 4 | import type { Comment } from "@/api/blog";
|
|---|
| 5 |
|
|---|
| 6 | interface CommentSectionProps {
|
|---|
| 7 | comments: Comment[];
|
|---|
| 8 | commentsCount: number;
|
|---|
| 9 | onAddComment: (content: string) => Promise<void>;
|
|---|
| 10 | onUpdateComment?: (commentId: number, content: string) => Promise<void>;
|
|---|
| 11 | onDeleteComment?: (commentId: number) => Promise<void>;
|
|---|
| 12 | }
|
|---|
| 13 |
|
|---|
| 14 | const CommentSection: Component<CommentSectionProps> = (props) => {
|
|---|
| 15 | const { user } = useAuth();
|
|---|
| 16 | const [newComment, setNewComment] = createSignal("");
|
|---|
| 17 | const [editingCommentId, setEditingCommentId] = createSignal<number | null>(
|
|---|
| 18 | null,
|
|---|
| 19 | );
|
|---|
| 20 | const [editContent, setEditContent] = createSignal("");
|
|---|
| 21 |
|
|---|
| 22 | const handleSubmit = async (e: Event) => {
|
|---|
| 23 | e.preventDefault();
|
|---|
| 24 | if (!newComment().trim()) return;
|
|---|
| 25 |
|
|---|
| 26 | await props.onAddComment(newComment());
|
|---|
| 27 | setNewComment("");
|
|---|
| 28 | };
|
|---|
| 29 |
|
|---|
| 30 | const startEdit = (comment: Comment) => {
|
|---|
| 31 | setEditingCommentId(comment.idComment);
|
|---|
| 32 | setEditContent(comment.content);
|
|---|
| 33 | };
|
|---|
| 34 |
|
|---|
| 35 | const cancelEdit = () => {
|
|---|
| 36 | setEditingCommentId(null);
|
|---|
| 37 | setEditContent("");
|
|---|
| 38 | };
|
|---|
| 39 |
|
|---|
| 40 | const saveEdit = async (commentId: number) => {
|
|---|
| 41 | if (props.onUpdateComment && editContent().trim()) {
|
|---|
| 42 | await props.onUpdateComment(commentId, editContent());
|
|---|
| 43 | setEditingCommentId(null);
|
|---|
| 44 | setEditContent("");
|
|---|
| 45 | }
|
|---|
| 46 | };
|
|---|
| 47 |
|
|---|
| 48 | const handleDelete = async (commentId: number) => {
|
|---|
| 49 | if (
|
|---|
| 50 | props.onDeleteComment &&
|
|---|
| 51 | confirm("Are you sure you want to delete this comment?")
|
|---|
| 52 | ) {
|
|---|
| 53 | await props.onDeleteComment(commentId);
|
|---|
| 54 | }
|
|---|
| 55 | };
|
|---|
| 56 |
|
|---|
| 57 | const isCommentOwner = (comment: Comment) =>
|
|---|
| 58 | user()?.userId === comment.patientId;
|
|---|
| 59 |
|
|---|
| 60 | const sortedComments = () => {
|
|---|
| 61 | if (!props.comments) return [];
|
|---|
| 62 |
|
|---|
| 63 | return [...props.comments].sort(
|
|---|
| 64 | (a, b) =>
|
|---|
| 65 | new Date(b.dateOfComment).getTime() -
|
|---|
| 66 | new Date(a.dateOfComment).getTime(),
|
|---|
| 67 | );
|
|---|
| 68 | };
|
|---|
| 69 |
|
|---|
| 70 | return (
|
|---|
| 71 | <div class="space-y-4">
|
|---|
| 72 | <h3 class="text-xl font-semibold text-gray-900 mb-4">Comments</h3>
|
|---|
| 73 |
|
|---|
| 74 | <form onSubmit={handleSubmit} class="mb-6">
|
|---|
| 75 | <textarea
|
|---|
| 76 | rows={3}
|
|---|
| 77 | class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|---|
| 78 | placeholder="Write a comment..."
|
|---|
| 79 | value={newComment()}
|
|---|
| 80 | onInput={(e) => setNewComment(e.currentTarget.value)}
|
|---|
| 81 | />
|
|---|
| 82 | <button
|
|---|
| 83 | type="submit"
|
|---|
| 84 | disabled={!newComment().trim()}
|
|---|
| 85 | class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 transition-colors cursor-pointer"
|
|---|
| 86 | >
|
|---|
| 87 | Post Comment
|
|---|
| 88 | </button>
|
|---|
| 89 | </form>
|
|---|
| 90 |
|
|---|
| 91 | <Show when={props.commentsCount === 0}>
|
|---|
| 92 | <p class="text-gray-500 text-center py-4">
|
|---|
| 93 | No comments yet. Be the first to comment!
|
|---|
| 94 | </p>
|
|---|
| 95 | </Show>
|
|---|
| 96 |
|
|---|
| 97 | <Show when={(props.comments?.length || 0) > 0}>
|
|---|
| 98 | <div class="space-y-4">
|
|---|
| 99 | <For each={sortedComments()}>
|
|---|
| 100 | {(comment) => (
|
|---|
| 101 | <div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
|---|
| 102 | <div class="flex justify-between items-start mb-2">
|
|---|
| 103 | <span class="font-semibold text-gray-900">
|
|---|
| 104 | {comment.patientName}
|
|---|
| 105 | </span>
|
|---|
| 106 | <div class="flex items-center gap-2">
|
|---|
| 107 | <span class="text-xs text-gray-500">
|
|---|
| 108 | {formatDateTime(comment.dateOfComment)}
|
|---|
| 109 | </span>
|
|---|
| 110 | <Show
|
|---|
| 111 | when={
|
|---|
| 112 | isCommentOwner(comment) &&
|
|---|
| 113 | editingCommentId() !== comment.idComment
|
|---|
| 114 | }
|
|---|
| 115 | >
|
|---|
| 116 | <button
|
|---|
| 117 | onClick={() => startEdit(comment)}
|
|---|
| 118 | class="text-gray-400 hover:text-blue-600 transition-colors cursor-pointer"
|
|---|
| 119 | title="Edit comment"
|
|---|
| 120 | >
|
|---|
| 121 | <svg
|
|---|
| 122 | class="w-4 h-4"
|
|---|
| 123 | fill="none"
|
|---|
| 124 | stroke="currentColor"
|
|---|
| 125 | viewBox="0 0 24 24"
|
|---|
| 126 | >
|
|---|
| 127 | <path
|
|---|
| 128 | stroke-linecap="round"
|
|---|
| 129 | stroke-linejoin="round"
|
|---|
| 130 | stroke-width="2"
|
|---|
| 131 | d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|---|
| 132 | />
|
|---|
| 133 | </svg>
|
|---|
| 134 | </button>
|
|---|
| 135 | <button
|
|---|
| 136 | onClick={() => handleDelete(comment.idComment)}
|
|---|
| 137 | class="text-gray-400 hover:text-red-600 transition-colors cursor-pointer"
|
|---|
| 138 | title="Delete comment"
|
|---|
| 139 | >
|
|---|
| 140 | <svg
|
|---|
| 141 | class="w-4 h-4"
|
|---|
| 142 | fill="none"
|
|---|
| 143 | stroke="currentColor"
|
|---|
| 144 | viewBox="0 0 24 24"
|
|---|
| 145 | >
|
|---|
| 146 | <path
|
|---|
| 147 | stroke-linecap="round"
|
|---|
| 148 | stroke-linejoin="round"
|
|---|
| 149 | stroke-width="2"
|
|---|
| 150 | d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|---|
| 151 | />
|
|---|
| 152 | </svg>
|
|---|
| 153 | </button>
|
|---|
| 154 | </Show>
|
|---|
| 155 | </div>
|
|---|
| 156 | </div>
|
|---|
| 157 | <Show
|
|---|
| 158 | when={editingCommentId() !== comment.idComment}
|
|---|
| 159 | fallback={
|
|---|
| 160 | <div class="space-y-2">
|
|---|
| 161 | <textarea
|
|---|
| 162 | value={editContent()}
|
|---|
| 163 | onInput={(e) => setEditContent(e.currentTarget.value)}
|
|---|
| 164 | class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
|---|
| 165 | rows={3}
|
|---|
| 166 | />
|
|---|
| 167 | <div class="flex gap-2">
|
|---|
| 168 | <button
|
|---|
| 169 | onClick={() => saveEdit(comment.idComment)}
|
|---|
| 170 | class="px-3 py-1 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors cursor-pointer"
|
|---|
| 171 | >
|
|---|
| 172 | Save
|
|---|
| 173 | </button>
|
|---|
| 174 | <button
|
|---|
| 175 | onClick={cancelEdit}
|
|---|
| 176 | class="px-3 py-1 text-sm bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors cursor-pointer"
|
|---|
| 177 | >
|
|---|
| 178 | Cancel
|
|---|
| 179 | </button>
|
|---|
| 180 | </div>
|
|---|
| 181 | </div>
|
|---|
| 182 | }
|
|---|
| 183 | >
|
|---|
| 184 | <p class="text-gray-700 whitespace-pre-wrap">
|
|---|
| 185 | {comment.content}
|
|---|
| 186 | </p>
|
|---|
| 187 | </Show>
|
|---|
| 188 | </div>
|
|---|
| 189 | )}
|
|---|
| 190 | </For>
|
|---|
| 191 | </div>
|
|---|
| 192 | </Show>
|
|---|
| 193 | </div>
|
|---|
| 194 | );
|
|---|
| 195 | };
|
|---|
| 196 |
|
|---|
| 197 | export default CommentSection;
|
|---|