diff --git a/frontend/src/components/features/comments/CommentItem.tsx b/frontend/src/components/features/comments/CommentItem.tsx
new file mode 100644
index 0000000..94c9efc
--- /dev/null
+++ b/frontend/src/components/features/comments/CommentItem.tsx
@@ -0,0 +1,154 @@
+import React, { useState } from 'react';
+import { useAuth } from '../../../contexts/AuthContext';
+import { useCreateComment } from '../../../queries/comments';
+import { Button } from '../../ui/button';
+import { Textarea } from '../../ui/textarea';
+import { Card, CardContent } from '../../ui/card';
+import { format } from 'date-fns';
+import { mk } from 'date-fns/locale';
+import type { Comment } from '../../../lib/api';
+
+interface CommentItemProps {
+ comment: Comment;
+ articleId?: string;
+ liveBlogId?: string;
+ depth?: number;
+}
+
+export function CommentItem({ comment, articleId, liveBlogId, depth = 0 }: CommentItemProps) {
+ const { isAuthenticated } = useAuth();
+ const [showReplyForm, setShowReplyForm] = useState(false);
+ const [replyContent, setReplyContent] = useState('');
+ const [isSubmittingReply, setIsSubmittingReply] = useState(false);
+
+ const createCommentMutation = useCreateComment();
+
+ const handleSubmitReply = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!replyContent.trim() || !isAuthenticated) return;
+
+ setIsSubmittingReply(true);
+ try {
+ await createCommentMutation.mutateAsync({
+ content: replyContent,
+ articleId,
+ liveBlogId,
+ parentCommentId: comment.id,
+ });
+ setReplyContent('');
+ setShowReplyForm(false);
+ } catch (error) {
+ console.error('Failed to post reply:', error);
+ } finally {
+ setIsSubmittingReply(false);
+ }
+ };
+
+ // Maximum depth to prevent infinite nesting (optional)
+ const maxDepth = 5;
+ const canReply = depth < maxDepth;
+
+ return (
+
0 ? 'ml-8 mt-4 border-l-2 border-border pl-4' : ''}>
+
+
+
+
+
+ {comment.user?.username || 'Анонимен корисник'}
+
+
+ {format(new Date(comment.createdAt), 'dd MMMM yyyy, HH:mm', { locale: mk })}
+
+
+ {comment.user?.role === 'admin' && (
+
+ Администратор
+
+ )}
+
+
+ {comment.content}
+
+ {/* Reply button and form */}
+ {isAuthenticated && canReply && (
+
+ {!showReplyForm ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Reactions (if implemented) */}
+ {comment.reactions && (
+
+
+ 👍 {comment.reactions.likes}
+
+
+ 👎 {comment.reactions.dislikes}
+
+
+ )}
+
+
+
+ {/* Render replies recursively */}
+ {comment.replies && comment.replies.length > 0 && (
+
+ {comment.replies.map((reply) => (
+
+ ))}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/features/comments/CommentSection.tsx b/frontend/src/components/features/comments/CommentSection.tsx
index 695e795..79b68ed 100644
--- a/frontend/src/components/features/comments/CommentSection.tsx
+++ b/frontend/src/components/features/comments/CommentSection.tsx
@@ -4,8 +4,7 @@ import { useComments, useCreateComment } from '../../../queries/comments';
import { Button } from '../../ui/button';
import { Textarea } from '../../ui/textarea';
import { Card, CardContent } from '../../ui/card';
-import { format } from 'date-fns';
-import { mk } from 'date-fns/locale';
+import { CommentItem } from './CommentItem';
interface CommentSectionProps {
articleId?: string;
@@ -99,29 +98,15 @@ export function CommentSection({ articleId, liveBlogId }: CommentSectionProps) {
- ) : (
+ ) : (
{comments.map((comment) => (
-
-
-
-
-
- {comment.user?.username || 'Анонимен корисник'}
-
-
- {format(new Date(comment.createdAt), 'dd MMMM yyyy, HH:mm', { locale: mk })}
-
-
- {comment.user?.role === 'admin' && (
-
- Администратор
-
- )}
-
- {comment.content}
-
-
+
))}
)}
diff --git a/frontend/src/hooks/useLiveBlogStream.ts b/frontend/src/hooks/useLiveBlogStream.ts
index 6530e79..92b4f61 100644
--- a/frontend/src/hooks/useLiveBlogStream.ts
+++ b/frontend/src/hooks/useLiveBlogStream.ts
@@ -16,11 +16,11 @@ export function useLiveBlogStream(
liveBlogId: string,
options: LiveBlogStreamOptions = {}
) {
- const defaultOptions: LiveBlogStreamOptions = {
+ const defaultOptions = useMemo((): LiveBlogStreamOptions => ({
autoReconnect: true,
reconnectInterval: 3000,
maxReconnectAttempts: 10,
- };
+ }), []);
const mergedOptions = useMemo(() => ({ ...defaultOptions, ...options }), [options, defaultOptions]);
const [isConnected, setIsConnected] = useState(false);
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 99bc8e3..b7c756b 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -578,6 +578,7 @@ export interface Comment {
likes: number;
dislikes: number;
};
+ replies?: Comment[];
createdAt: string;
updatedAt: string;
}
@@ -622,6 +623,42 @@ export interface CreateReactionDto {
}
// Comment API Functions
+// Interface for backend comment response
+interface BackendComment {
+ id: string;
+ content: string;
+ articleId: string | null;
+ liveBlogId: string | null;
+ parentId: string | null;
+ userId: string;
+ likeCount: number;
+ dislikeCount: number;
+ isVisible: boolean;
+ createdAt: string;
+ updatedAt: string;
+ user?: {
+ id: string;
+ username: string;
+ };
+ replies?: BackendComment[];
+}
+
+// Recursive function to map comment and its replies
+function mapBackendComment(comment: BackendComment): Comment {
+ const mappedComment: Comment = {
+ ...comment,
+ parentCommentId: comment.parentId,
+ // Ensure reactions object exists
+ reactions: {
+ likes: comment.likeCount || 0,
+ dislikes: comment.dislikeCount || 0,
+ },
+ // Recursively map replies if they exist
+ replies: comment.replies?.map(mapBackendComment) || [],
+ };
+ return mappedComment;
+}
+
export async function fetchComments(params: FindCommentsParams = {}): Promise {
const searchParams = new URLSearchParams();
@@ -649,16 +686,7 @@ export async function fetchComments(params: FindCommentsParams = {}): Promise ({
- ...comment,
- parentCommentId: comment.parentId,
- // Ensure reactions object exists
- reactions: {
- likes: comment.likeCount || 0,
- dislikes: comment.dislikeCount || 0,
- },
- }));
+ const mappedData = (data as BackendComment[]).map(mapBackendComment);
return {
data: mappedData,
@@ -683,18 +711,10 @@ export async function createComment(dto: CreateCommentDto): Promise {
throw new Error('Failed to create comment');
}
- const comment = await response.json();
+ const comment = await response.json() as BackendComment;
// Map backend response to frontend interface
- return {
- ...comment,
- parentCommentId: comment.parentId,
- // Ensure reactions object exists
- reactions: {
- likes: comment.likeCount || 0,
- dislikes: comment.dislikeCount || 0,
- },
- };
+ return mapBackendComment(comment);
}
export async function updateComment(id: string, dto: UpdateCommentDto): Promise {