diff --git a/client/index.html b/client/index.html index 747262d..e0e1d11 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ - Catstagram + ΠšΠΎΡ‚ΠΎΠ³Ρ€Π°ΠΌ diff --git a/client/src/api/endpoints.ts b/client/src/api/endpoints.ts index 630b6bb..1e8eb46 100644 --- a/client/src/api/endpoints.ts +++ b/client/src/api/endpoints.ts @@ -10,6 +10,15 @@ export interface Cat { username: string; user_points: number; likes_count: number; + comments_count: number; +} + +export interface Comment { + id: number; + text: string; + created_at: string; + user_id: number; + username: string; } export interface AuthResponse { @@ -87,3 +96,16 @@ export function adminDeleteCat(id: number) { export function adminDeleteUser(id: number) { return api.delete(`/admin/users/${id}`); } + +// Comments +export function getComments(catId: number) { + return api.get<{ comments: Comment[] }>(`/comments/${catId}`); +} + +export function addComment(catId: number, text: string) { + return api.post<{ comment: Comment }>(`/comments/${catId}`, { text }); +} + +export function deleteComment(commentId: number) { + return api.delete(`/comments/${commentId}`); +} diff --git a/client/src/components/CatCard.tsx b/client/src/components/CatCard.tsx index 8d5d0e3..db4826d 100644 --- a/client/src/components/CatCard.tsx +++ b/client/src/components/CatCard.tsx @@ -90,6 +90,7 @@ export default function CatCard({ cat, onOpen }: CatCardProps) { className="flex items-center gap-1 text-sm text-[var(--fg-tertiary)] hover:text-[var(--fg-secondary)] transition-colors" > + {(cat.comments_count ?? 0) > 0 && {cat.comments_count}} diff --git a/client/src/components/CatModal.tsx b/client/src/components/CatModal.tsx index 3fe1773..8c0f165 100644 --- a/client/src/components/CatModal.tsx +++ b/client/src/components/CatModal.tsx @@ -1,10 +1,11 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; 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 { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { X, Heart, Trash2, Trophy } from 'lucide-react'; +import { X, Heart, Trash2, Trophy, Send } from 'lucide-react'; interface CatModalProps { catId: number; @@ -18,15 +19,35 @@ export default function CatModal({ catId, onClose }: CatModalProps) { 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 { toast } = useToast(); const [liked, setLiked] = useState(false); const [likeCount, setLikeCount] = useState(0); const [imageLoaded, setImageLoaded] = useState(false); + const [commentText, setCommentText] = useState(''); + const commentsEndRef = useRef(null); useEffect(() => { if (likeData) setLiked(likeData.liked); }, [likeData]); useEffect(() => { if (data?.cat) setLikeCount(data.cat.likes_count); }, [data]); - useEffect(() => { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, []); + useEffect(() => { + const scrollY = window.scrollY; + document.body.style.position = 'fixed'; + document.body.style.top = `-${scrollY}px`; + document.body.style.left = '0'; + document.body.style.right = '0'; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.right = ''; + document.body.style.overflow = ''; + window.scrollTo(0, scrollY); + }; + }, []); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', h); @@ -34,6 +55,7 @@ export default function CatModal({ catId, onClose }: CatModalProps) { }, [onClose]); const cat = data?.cat; + const comments = commentsData?.comments ?? []; const isOwner = cat && user && cat.user_id === user.id; const toggleLike = async () => { @@ -56,6 +78,21 @@ export default function CatModal({ catId, onClose }: CatModalProps) { 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 || !cat) { return (
@@ -110,26 +147,60 @@ export default function CatModal({ catId, onClose }: CatModalProps) { {cat.caption setImageLoaded(true)} />
-
-
- - - {cat.username.charAt(0).toUpperCase()} - - -

- @{cat.username} - {cat.caption || 'Π‘Π΅Π· подписи'} -

-
+
+ {cat.caption && ( +
+ + + {cat.username.charAt(0).toUpperCase()} + + +

+ @{cat.username} + {cat.caption} +

+
+ )} + + {comments.length > 0 && ( +
+ {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)) && ( + + )} +
+ ))} +
+
+ )}
-
+
+ + {user && ( +
{ e.preventDefault(); handleComment(); }} + className="flex items-center gap-2" + > + setCommentText(e.target.value)} + placeholder="ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ..." + maxLength={500} + className="flex-1 h-9 px-3 text-sm rounded-xl border border-[var(--border)] bg-white placeholder:text-[var(--fg-tertiary)] focus-ring" + /> + +
+ )}
diff --git a/client/src/components/FeedSidebar.tsx b/client/src/components/FeedSidebar.tsx index d1a328b..b042b41 100644 --- a/client/src/components/FeedSidebar.tsx +++ b/client/src/components/FeedSidebar.tsx @@ -70,7 +70,7 @@ export default function FeedSidebar() { )}
-

Catstagram Β· сообщСство ΠΊΠΎΡ‚ΠΎΠ²ΠΎΠ΄ΠΎΠ²

+

ΠšΠΎΡ‚ΠΎΠ³Ρ€Π°ΠΌ Β· сообщСство ΠΊΠΎΡ‚ΠΎΠ²ΠΎΠ΄ΠΎΠ²

); } \ No newline at end of file diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index 851d5a5..a7feba1 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -13,7 +13,7 @@ export default function Navbar() {