- Timestamp:
- 03/24/26 22:13:36 (3 months ago)
- Branches:
- main
- Children:
- 7fbb91c
- Parents:
- acf690c
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
chapterx-frontend/src/components/story/CommentSection.tsx
racf690c r73b69b2 1 import React, { use State } from 'react'1 import React, { useEffect, useState } from 'react' 2 2 import { MessageCircle, Trash2, Send } from 'lucide-react' 3 import axios from 'axios' 3 4 import { useAuthStore } from '../../store/authStore' 4 import { useStoryStore } from '../../store/storyStore'5 5 import { useNotificationStore } from '../../store/notificationStore' 6 6 import { useUIStore } from '../../store/uiStore' 7 import { Comment } from '../../types'8 7 import { Avatar } from '../ui/Avatar' 9 8 import { Button } from '../ui/Button' 10 9 11 interface CommentSectionProps { 12 storyId: number 13 authorUserId: number 10 const API = 'https://localhost:7125/api' 11 12 function getAuthHeaders() { 13 try { 14 const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token 15 return token ? { Authorization: `Bearer ${token}` } : {} 16 } catch { return {} } 14 17 } 15 18 … … 24 27 } 25 28 26 export const CommentSection: React.FC<CommentSectionProps> = ({ storyId, authorUserId }) => { 29 interface BackendComment { 30 id: number 31 content: string 32 userId: number 33 storyId: number 34 username: string 35 createdAt: string 36 } 37 38 interface CommentSectionProps { 39 storyId: number 40 authorUserId: number 41 onCountChange?: (count: number) => void 42 } 43 44 export const CommentSection: React.FC<CommentSectionProps> = ({ storyId, authorUserId, onCountChange }) => { 27 45 const { currentUser } = useAuthStore() 28 const { comments, addComment, deleteComment } = useStoryStore()29 46 const { addNotification } = useNotificationStore() 30 47 const { addToast } = useUIStore() 48 const [comments, setComments] = useState<BackendComment[]>([]) 31 49 const [text, setText] = useState('') 32 50 const [submitting, setSubmitting] = useState(false) 33 51 34 const storyComments = comments 35 .filter(c => c.story_id === storyId) 36 .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) 52 useEffect(() => { 53 axios.get(`${API}/comments/story/${storyId}`) 54 .then(res => { 55 const data = res.data ?? [] 56 setComments(data) 57 onCountChange?.(data.length) 58 }) 59 .catch(() => {}) 60 }, [storyId]) 37 61 38 62 const handleSubmit = async () => { 39 63 if (!text.trim() || !currentUser) return 40 64 setSubmitting(true) 41 await new Promise(r => setTimeout(r, 300)) 42 const comment: Comment = { 43 comment_id: Date.now(), 44 story_id: storyId, 45 user_id: currentUser.user_id, 46 username: currentUser.username, 47 content: text.trim(), 48 created_at: new Date().toISOString(), 65 try { 66 const res = await axios.post(`${API}/comments`, { 67 content: text.trim(), 68 userId: currentUser.user_id, 69 storyId, 70 }, { headers: getAuthHeaders() }) 71 const newComment: BackendComment = { 72 id: res.data?.id ?? Date.now(), 73 content: text.trim(), 74 userId: currentUser.user_id, 75 storyId, 76 username: currentUser.username, 77 createdAt: new Date().toISOString(), 78 } 79 setComments(prev => { const next = [newComment, ...prev]; onCountChange?.(next.length); return next }) 80 if (currentUser.user_id !== authorUserId) { 81 await addNotification({ 82 recipientUserId: authorUserId, 83 type: 'comment', 84 content: `${currentUser.username} commented: "${text.trim().slice(0, 60)}${text.length > 60 ? '...' : ''}"`, 85 link: `/story/${storyId}`, 86 }) 87 } 88 setText('') 89 addToast('Comment posted!') 90 } catch { 91 addToast('Failed to post comment.', 'error') 49 92 } 50 addComment(comment) 51 if (currentUser.user_id !== authorUserId) { 52 addNotification({ 53 user_id: authorUserId, 54 type: 'comment', 55 title: 'New Comment', 56 message: `${currentUser.username} commented: "${text.trim().slice(0, 60)}..."`, 57 link: `/story/${storyId}`, 58 }) 93 setSubmitting(false) 94 } 95 96 const handleDelete = async (commentId: number) => { 97 try { 98 await axios.delete(`${API}/comments/${commentId}`, { headers: getAuthHeaders() }) 99 setComments(prev => { const next = prev.filter(c => c.id !== commentId); onCountChange?.(next.length); return next }) 100 addToast('Comment deleted', 'info') 101 } catch { 102 addToast('Failed to delete comment.', 'error') 59 103 } 60 setText('')61 setSubmitting(false)62 addToast('Comment posted!')63 104 } 64 105 … … 67 108 <div className="flex items-center gap-2 mb-4"> 68 109 <MessageCircle size={18} className="text-indigo-400" /> 69 <h3 className="text-white font-semibold">Comments ({ storyComments.length})</h3>110 <h3 className="text-white font-semibold">Comments ({comments.length})</h3> 70 111 </div> 71 112 72 {/* Input */}73 113 {currentUser ? ( 74 114 <div className="flex gap-3"> … … 83 123 /> 84 124 <div className="flex justify-end mt-2"> 85 <Button 86 size="sm" 87 onClick={handleSubmit} 88 loading={submitting} 89 disabled={!text.trim()} 90 > 125 <Button size="sm" onClick={handleSubmit} loading={submitting} disabled={!text.trim()}> 91 126 <Send size={14} /> 92 127 Post Comment … … 103 138 )} 104 139 105 {/* Comment list */}106 140 <div className="space-y-3"> 107 { storyComments.length === 0 ? (141 {comments.length === 0 ? ( 108 142 <div className="text-center py-8 text-slate-500 text-sm"> 109 143 No comments yet. Be the first to share your thoughts! 110 144 </div> 111 145 ) : ( 112 storyComments.map(comment => ( 113 <CommentCard 114 key={comment.comment_id} 115 comment={comment} 116 canDelete={currentUser?.user_id === comment.user_id || currentUser?.role === 'admin'} 117 onDelete={() => { deleteComment(comment.comment_id); addToast('Comment deleted', 'info') }} 118 /> 146 comments.map(comment => ( 147 <div key={comment.id} className="flex gap-3 p-4 bg-slate-800 rounded-xl border border-slate-700"> 148 <Avatar name={comment.username} size="sm" /> 149 <div className="flex-1 min-w-0"> 150 <div className="flex items-center justify-between"> 151 <span className="text-sm font-medium text-white">{comment.username}</span> 152 <div className="flex items-center gap-2"> 153 <span className="text-xs text-slate-500">{timeAgo(comment.createdAt)}</span> 154 {(currentUser?.user_id === comment.userId || currentUser?.role === 'admin') && ( 155 <button onClick={() => handleDelete(comment.id)} className="text-slate-500 hover:text-rose-400 transition-colors"> 156 <Trash2 size={13} /> 157 </button> 158 )} 159 </div> 160 </div> 161 <p className="text-slate-300 text-sm mt-1 leading-relaxed">{comment.content}</p> 162 </div> 163 </div> 119 164 )) 120 165 )} … … 123 168 ) 124 169 } 125 126 const CommentCard: React.FC<{127 comment: Comment128 canDelete: boolean129 onDelete: () => void130 }> = ({ comment, canDelete, onDelete }) => (131 <div className="flex gap-3 p-4 bg-slate-800 rounded-xl border border-slate-700">132 <Avatar name={comment.username} size="sm" />133 <div className="flex-1 min-w-0">134 <div className="flex items-center justify-between">135 <span className="text-sm font-medium text-white">{comment.username}</span>136 <div className="flex items-center gap-2">137 <span className="text-xs text-slate-500">{timeAgo(comment.created_at)}</span>138 {canDelete && (139 <button onClick={onDelete} className="text-slate-500 hover:text-rose-400 transition-colors">140 <Trash2 size={13} />141 </button>142 )}143 </div>144 </div>145 <p className="text-slate-300 text-sm mt-1 leading-relaxed">{comment.content}</p>146 </div>147 </div>148 )
Note:
See TracChangeset
for help on using the changeset viewer.
