Ignore:
Timestamp:
03/24/26 22:13:36 (3 months ago)
Author:
kikisrbinoska <srbinoskakristina07@…>
Branches:
main
Children:
7fbb91c
Parents:
acf690c
Message:

Fixed reading lists,comments and likes

Location:
chapterx-frontend/src/components/story
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • chapterx-frontend/src/components/story/CommentSection.tsx

    racf690c r73b69b2  
    1 import React, { useState } from 'react'
     1import React, { useEffect, useState } from 'react'
    22import { MessageCircle, Trash2, Send } from 'lucide-react'
     3import axios from 'axios'
    34import { useAuthStore } from '../../store/authStore'
    4 import { useStoryStore } from '../../store/storyStore'
    55import { useNotificationStore } from '../../store/notificationStore'
    66import { useUIStore } from '../../store/uiStore'
    7 import { Comment } from '../../types'
    87import { Avatar } from '../ui/Avatar'
    98import { Button } from '../ui/Button'
    109
    11 interface CommentSectionProps {
    12   storyId: number
    13   authorUserId: number
     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 {} }
    1417}
    1518
     
    2427}
    2528
    26 export const CommentSection: React.FC<CommentSectionProps> = ({ storyId, authorUserId }) => {
     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 }) => {
    2745  const { currentUser } = useAuthStore()
    28   const { comments, addComment, deleteComment } = useStoryStore()
    2946  const { addNotification } = useNotificationStore()
    3047  const { addToast } = useUIStore()
     48  const [comments, setComments] = useState<BackendComment[]>([])
    3149  const [text, setText] = useState('')
    3250  const [submitting, setSubmitting] = useState(false)
    3351
    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])
    3761
    3862  const handleSubmit = async () => {
    3963    if (!text.trim() || !currentUser) return
    4064    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')
    4992    }
    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')
    59103    }
    60     setText('')
    61     setSubmitting(false)
    62     addToast('Comment posted!')
    63104  }
    64105
     
    67108      <div className="flex items-center gap-2 mb-4">
    68109        <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>
    70111      </div>
    71112
    72       {/* Input */}
    73113      {currentUser ? (
    74114        <div className="flex gap-3">
     
    83123            />
    84124            <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()}>
    91126                <Send size={14} />
    92127                Post Comment
     
    103138      )}
    104139
    105       {/* Comment list */}
    106140      <div className="space-y-3">
    107         {storyComments.length === 0 ? (
     141        {comments.length === 0 ? (
    108142          <div className="text-center py-8 text-slate-500 text-sm">
    109143            No comments yet. Be the first to share your thoughts!
    110144          </div>
    111145        ) : (
    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>
    119164          ))
    120165        )}
     
    123168  )
    124169}
    125 
    126 const CommentCard: React.FC<{
    127   comment: Comment
    128   canDelete: boolean
    129   onDelete: () => void
    130 }> = ({ 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 )
  • chapterx-frontend/src/components/story/LikeButton.tsx

    racf690c r73b69b2  
    1 import React from 'react'
     1import React, { useEffect, useState } from 'react'
    22import { Heart } from 'lucide-react'
    33import { useNavigate } from 'react-router-dom'
     4import axios from 'axios'
    45import { useAuthStore } from '../../store/authStore'
    5 import { useStoryStore } from '../../store/storyStore'
    66import { useNotificationStore } from '../../store/notificationStore'
    77import { useUIStore } from '../../store/uiStore'
     8
     9const API = 'https://localhost:7125/api'
     10
     11function getAuthHeaders() {
     12  try {
     13    const token = JSON.parse(localStorage.getItem('chapterx-auth') || '{}')?.state?.token
     14    return token ? { Authorization: `Bearer ${token}` } : {}
     15  } catch { return {} }
     16}
    817
    918interface LikeButtonProps {
     
    1120  authorUserId: number
    1221  totalLikes: number
     22  onCountChange?: (count: number) => void
    1323}
    1424
    15 export const LikeButton: React.FC<LikeButtonProps> = ({ storyId, authorUserId, totalLikes }) => {
     25export const LikeButton: React.FC<LikeButtonProps> = ({ storyId, authorUserId, totalLikes, onCountChange }) => {
    1626  const navigate = useNavigate()
    1727  const { currentUser } = useAuthStore()
    18   const { toggleLike, isLiked } = useStoryStore()
    1928  const { addNotification } = useNotificationStore()
    2029  const { addToast } = useUIStore()
     30  const [liked, setLiked] = useState(false)
     31  const [count, setCount] = useState(totalLikes)
     32  const [loading, setLoading] = useState(false)
    2133
    22   const liked = currentUser ? isLiked(currentUser.user_id, storyId) : false
     34  useEffect(() => {
     35    axios.get(`${API}/likes/story/${storyId}`)
     36      .then(res => {
     37        const c = res.data?.count ?? totalLikes
     38        setCount(c)
     39        onCountChange?.(c)
     40        if (currentUser) {
     41          setLiked((res.data?.userIds ?? []).includes(currentUser.user_id))
     42        }
     43      })
     44      .catch(() => {})
     45  }, [storyId, currentUser?.user_id])
    2346
    24   const handleClick = () => {
     47  const handleClick = async () => {
    2548    if (!currentUser) {
    2649      addToast('Please sign in to like stories.', 'info')
     
    2851      return
    2952    }
    30     toggleLike(currentUser.user_id, storyId)
    31     if (!liked && currentUser.user_id !== authorUserId) {
    32       addNotification({
    33         user_id: authorUserId,
    34         type: 'like',
    35         title: 'New Like',
    36         message: `${currentUser.username} liked your story.`,
    37         link: `/story/${storyId}`,
    38       })
     53    if (loading) return
     54    setLoading(true)
     55    try {
     56      if (liked) {
     57        await axios.delete(`${API}/likes/user/${currentUser.user_id}/story/${storyId}`, { headers: getAuthHeaders() })
     58        setLiked(false)
     59        setCount(c => { const n = c - 1; onCountChange?.(n); return n })
     60        addToast('Removed from likes', 'info')
     61      } else {
     62        await axios.post(`${API}/likes`, { userId: currentUser.user_id, storyId }, { headers: getAuthHeaders() })
     63        setLiked(true)
     64        setCount(c => { const n = c + 1; onCountChange?.(n); return n })
     65        if (currentUser.user_id !== authorUserId) {
     66          await addNotification({
     67            recipientUserId: authorUserId,
     68            type: 'like',
     69            content: `${currentUser.username} liked your story.`,
     70            link: `/story/${storyId}`,
     71          })
     72        }
     73        addToast('Added to likes!', 'success')
     74      }
     75    } catch {
     76      addToast('Something went wrong.', 'error')
    3977    }
    40     addToast(liked ? 'Removed from likes' : 'Added to likes!', liked ? 'info' : 'success')
     78    setLoading(false)
    4179  }
    4280
     
    4482    <button
    4583      onClick={handleClick}
     84      disabled={loading}
    4685      className={`flex items-center gap-2 px-4 py-2 rounded-xl border transition-all duration-200 ${
    4786        liked
     
    5190    >
    5291      <Heart size={16} className={liked ? 'fill-rose-400' : ''} />
    53       <span className="text-sm font-medium">{totalLikes.toLocaleString()}</span>
     92      <span className="text-sm font-medium">{count.toLocaleString()}</span>
    5493    </button>
    5594  )
Note: See TracChangeset for help on using the changeset viewer.