source: chapterx-frontend/src/components/story/CommentSection.tsx@ e882b92

main
Last change on this file since e882b92 was e882b92, checked in by kikisrbinoska <srbinoskakristina07@…>, 13 days ago

Added corrected entitef from the ER diagram and changes for the logic to be coresponding to the ddl

  • Property mode set to 100644
File size: 6.0 KB
RevLine 
[73b69b2]1import React, { useEffect, useState } from 'react'
[b62cefc]2import { MessageCircle, Trash2, Send } from 'lucide-react'
[73b69b2]3import axios from 'axios'
[b62cefc]4import { useAuthStore } from '../../store/authStore'
5import { useNotificationStore } from '../../store/notificationStore'
6import { useUIStore } from '../../store/uiStore'
7import { Avatar } from '../ui/Avatar'
8import { Button } from '../ui/Button'
9
[73b69b2]10const API = 'https://localhost:7125/api'
11
12function getAuthHeaders() {
13 try {
14 const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token
15 return token ? { Authorization: `Bearer ${token}` } : {}
16 } catch { return {} }
[b62cefc]17}
18
19function timeAgo(str: string): string {
20 const diff = Date.now() - new Date(str).getTime()
21 const m = Math.floor(diff / 60000)
22 if (m < 1) return 'just now'
23 if (m < 60) return `${m}m ago`
24 const h = Math.floor(m / 60)
25 if (h < 24) return `${h}h ago`
26 return `${Math.floor(h / 24)}d ago`
27}
28
[73b69b2]29interface BackendComment {
30 id: number
31 content: string
32 userId: number
33 storyId: number
34 username: string
35 createdAt: string
36}
37
38interface CommentSectionProps {
39 storyId: number
40 authorUserId: number
41 onCountChange?: (count: number) => void
42}
43
44export const CommentSection: React.FC<CommentSectionProps> = ({ storyId, authorUserId, onCountChange }) => {
[b62cefc]45 const { currentUser } = useAuthStore()
46 const { addNotification } = useNotificationStore()
47 const { addToast } = useUIStore()
[73b69b2]48 const [comments, setComments] = useState<BackendComment[]>([])
[b62cefc]49 const [text, setText] = useState('')
50 const [submitting, setSubmitting] = useState(false)
51
[73b69b2]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])
[b62cefc]61
62 const handleSubmit = async () => {
63 if (!text.trim() || !currentUser) return
64 setSubmitting(true)
[73b69b2]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({
[e882b92]82 userId: authorUserId,
83 contentType: 'comment',
[73b69b2]84 content: `${currentUser.username} commented: "${text.trim().slice(0, 60)}${text.length > 60 ? '...' : ''}"`,
[e882b92]85 storyId,
[73b69b2]86 link: `/story/${storyId}`,
87 })
88 }
89 setText('')
90 addToast('Comment posted!')
91 } catch {
92 addToast('Failed to post comment.', 'error')
[b62cefc]93 }
94 setSubmitting(false)
[73b69b2]95 }
96
97 const handleDelete = async (commentId: number) => {
98 try {
99 await axios.delete(`${API}/comments/${commentId}`, { headers: getAuthHeaders() })
100 setComments(prev => { const next = prev.filter(c => c.id !== commentId); onCountChange?.(next.length); return next })
101 addToast('Comment deleted', 'info')
102 } catch {
103 addToast('Failed to delete comment.', 'error')
104 }
[b62cefc]105 }
106
107 return (
108 <div className="space-y-4">
109 <div className="flex items-center gap-2 mb-4">
110 <MessageCircle size={18} className="text-indigo-400" />
[73b69b2]111 <h3 className="text-white font-semibold">Comments ({comments.length})</h3>
[b62cefc]112 </div>
113
114 {currentUser ? (
115 <div className="flex gap-3">
116 <Avatar name={currentUser.name} size="sm" />
117 <div className="flex-1">
118 <textarea
119 value={text}
120 onChange={e => setText(e.target.value)}
121 placeholder="Share your thoughts..."
122 rows={3}
123 className="w-full bg-slate-800 border border-slate-700 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500 resize-none"
124 />
125 <div className="flex justify-end mt-2">
[73b69b2]126 <Button size="sm" onClick={handleSubmit} loading={submitting} disabled={!text.trim()}>
[b62cefc]127 <Send size={14} />
128 Post Comment
129 </Button>
130 </div>
131 </div>
132 </div>
133 ) : (
134 <div className="bg-slate-800 border border-slate-700 rounded-xl p-4 text-center">
135 <p className="text-slate-400 text-sm">
136 <a href="/login" className="text-indigo-400 hover:text-indigo-300">Sign in</a> to leave a comment.
137 </p>
138 </div>
139 )}
140
141 <div className="space-y-3">
[73b69b2]142 {comments.length === 0 ? (
[b62cefc]143 <div className="text-center py-8 text-slate-500 text-sm">
144 No comments yet. Be the first to share your thoughts!
145 </div>
146 ) : (
[73b69b2]147 comments.map(comment => (
148 <div key={comment.id} className="flex gap-3 p-4 bg-slate-800 rounded-xl border border-slate-700">
149 <Avatar name={comment.username} size="sm" />
150 <div className="flex-1 min-w-0">
151 <div className="flex items-center justify-between">
152 <span className="text-sm font-medium text-white">{comment.username}</span>
153 <div className="flex items-center gap-2">
154 <span className="text-xs text-slate-500">{timeAgo(comment.createdAt)}</span>
155 {(currentUser?.user_id === comment.userId || currentUser?.role === 'admin') && (
156 <button onClick={() => handleDelete(comment.id)} className="text-slate-500 hover:text-rose-400 transition-colors">
157 <Trash2 size={13} />
158 </button>
159 )}
160 </div>
161 </div>
162 <p className="text-slate-300 text-sm mt-1 leading-relaxed">{comment.content}</p>
163 </div>
164 </div>
[b62cefc]165 ))
166 )}
167 </div>
168 </div>
169 )
170}
Note: See TracBrowser for help on using the repository browser.