diff --git a/client/src/hooks/useComments.ts b/client/src/hooks/useComments.ts
new file mode 100644
index 0000000..e21c92b
--- /dev/null
+++ b/client/src/hooks/useComments.ts
@@ -0,0 +1,34 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { getComments, addComment, deleteComment } from '@/api/endpoints';
+
+export function useComments(catId: number) {
+ return useQuery({
+ queryKey: ['comments', catId],
+ queryFn: () => getComments(catId).then(r => r.data),
+ enabled: !!catId,
+ });
+}
+
+export function useAddComment(catId: number) {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: (text: string) => addComment(catId, text).then(r => r.data),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['comments', catId] });
+ qc.invalidateQueries({ queryKey: ['cats'] });
+ qc.invalidateQueries({ queryKey: ['cat', catId] });
+ },
+ });
+}
+
+export function useDeleteComment(catId: number) {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: (commentId: number) => deleteComment(commentId),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['comments', catId] });
+ qc.invalidateQueries({ queryKey: ['cats'] });
+ qc.invalidateQueries({ queryKey: ['cat', catId] });
+ },
+ });
+}
\ No newline at end of file
diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts
index ede3b12..d514e53 100644
--- a/client/src/lib/auth.ts
+++ b/client/src/lib/auth.ts
@@ -1,5 +1,5 @@
-const TOKEN_KEY = 'catstagram_token';
-const USER_KEY = 'catstagram_user';
+const TOKEN_KEY = 'kotogram_token';
+const USER_KEY = 'kotogram_user';
export interface User {
id: number;
diff --git a/client/src/pages/CatPage.tsx b/client/src/pages/CatPage.tsx
index 70994ce..70ba9ad 100644
--- a/client/src/pages/CatPage.tsx
+++ b/client/src/pages/CatPage.tsx
@@ -1,13 +1,14 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { useCat, useDeleteCat } from '@/hooks/useCats';
import { useLikeStatus, useLikeCat, useUnlikeCat } from '@/hooks/useLikes';
+import { useComments, useAddComment, useDeleteComment } from '@/hooks/useComments';
import { useAuth } from '@/hooks/useAuth';
import { useToast } from '@/components/Toast';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
-import { ArrowLeft, Heart, Trash2, Trophy, Image } from 'lucide-react';
+import { ArrowLeft, Heart, Trash2, Trophy, Image, Send, X } from 'lucide-react';
export default function CatPage() {
const { id } = useParams<{ id: string }>();
@@ -21,10 +22,16 @@ export default function CatPage() {
const likeMutation = useLikeCat(catId);
const unlikeMutation = useUnlikeCat(catId);
const deleteMutation = useDeleteCat();
+ const { data: commentsData } = useComments(catId);
+ const addCommentMutation = useAddComment(catId);
+ const deleteCommentMutation = useDeleteComment(catId);
const [liked, setLiked] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
+ const [commentText, setCommentText] = useState('');
+ const commentsEndRef = useRef
(null);
const cat = data?.cat;
+ const comments = commentsData?.comments ?? [];
const isOwner = cat && user && cat.user_id === user.id;
const isLiked = likeData?.liked ?? false;
@@ -50,6 +57,21 @@ export default function CatPage() {
catch { toast('ΠΡΠΈΠ±ΠΊΠ° ΡΠ΄Π°Π»Π΅Π½ΠΈΡ', 'error'); }
};
+ const handleComment = async () => {
+ const text = commentText.trim();
+ if (!text || !user) return;
+ try {
+ await addCommentMutation.mutateAsync(text);
+ setCommentText('');
+ setTimeout(() => commentsEndRef.current?.scrollIntoView({ behavior: 'smooth' }), 100);
+ } catch { toast('ΠΡΠΈΠ±ΠΊΠ°', 'error'); }
+ };
+
+ const handleDeleteComment = async (commentId: number) => {
+ try { await deleteCommentMutation.mutateAsync(commentId); }
+ catch { toast('ΠΡΠΈΠ±ΠΊΠ°', 'error'); }
+ };
+
if (isLoading) {
return (
@@ -141,6 +163,61 @@ export default function CatPage() {
+ {comments.length > 0 && (
+
+
+ ΠΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΈ Β· {comments.length}
+
+ {comments.map(c => (
+
+
+
+ {c.username.charAt(0).toUpperCase()}
+
+
+
+
+ @{c.username}
+ {c.text}
+
+
+ {new Date(c.created_at).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}
+
+
+ {(user && (c.user_id === user.id || user.is_admin)) && (
+
handleDeleteComment(c.id)}
+ className="btn-ghost h-6 w-6 text-[var(--fg-tertiary)] hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
+ >
+
+
+ )}
+
+ ))}
+
+
+ )}
+
+ {user && (
+
+ )}
+
navigate('/feed')} className="rounded-xl text-xs h-8 px-4 gap-1">
Π Π»Π΅Π½ΡΡ
diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx
index d0443d2..9051b9b 100644
--- a/client/src/pages/LoginPage.tsx
+++ b/client/src/pages/LoginPage.tsx
@@ -30,7 +30,7 @@ export default function LoginPage() {
- Catstagram
+ ΠΠΎΡΠΎΠ³ΡΠ°ΠΌ
ΠΠΎΠΉΠ΄ΠΈΡΠ΅, ΡΡΠΎΠ±Ρ ΡΠΌΠΎΡΡΠ΅ΡΡ ΠΊΠΎΡΠΎΠ²
diff --git a/client/src/pages/RegisterPage.tsx b/client/src/pages/RegisterPage.tsx
index 30b8d2f..27ec7d6 100644
--- a/client/src/pages/RegisterPage.tsx
+++ b/client/src/pages/RegisterPage.tsx
@@ -32,7 +32,7 @@ export default function RegisterPage() {