diff --git a/client/src/App.tsx b/client/src/App.tsx index cbea52e..6eb6ec6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,6 +10,7 @@ import FeedPage from '@/pages/FeedPage'; import UploadPage from '@/pages/UploadPage'; import CatPage from '@/pages/CatPage'; import ProfilePage from '@/pages/ProfilePage'; +import AdminPage from '@/pages/AdminPage'; import { useEffect, useRef } from 'react'; const queryClient = new QueryClient({ @@ -82,6 +83,14 @@ function AnimatedRoutes() { } /> + + + + } + /> } /> } /> diff --git a/client/src/api/endpoints.ts b/client/src/api/endpoints.ts index ba02b39..29f81d5 100644 --- a/client/src/api/endpoints.ts +++ b/client/src/api/endpoints.ts @@ -52,13 +52,26 @@ export function deleteCat(id: number) { } export function likeCat(catId: number) { - return api.post<{ liked: boolean; likesCount: number; ownerPoints: number }>(`/likes/${catId}`); + return api.post<{ liked: boolean; likesCount: number }>(`/likes/${catId}`); } export function unlikeCat(catId: number) { - return api.delete<{ liked: boolean; likesCount: number; ownerPoints: number }>(`/likes/${catId}`); + return api.delete<{ liked: boolean; likesCount: number }>(`/likes/${catId}`); } export function getLikeStatus(catId: number) { return api.get<{ liked: boolean }>(`/likes/${catId}/status`); } + +// Admin +export function getAdminUsers() { + return api.get<{ users: User[] }>('/admin/users'); +} + +export function adminDeleteCat(id: number) { + return api.delete(`/admin/cats/${id}`); +} + +export function adminDeleteUser(id: number) { + return api.delete(`/admin/users/${id}`); +} diff --git a/client/src/components/CatCard.tsx b/client/src/components/CatCard.tsx index a02114b..f2b4c34 100644 --- a/client/src/components/CatCard.tsx +++ b/client/src/components/CatCard.tsx @@ -12,7 +12,7 @@ interface CatCardProps { } export default function CatCard({ cat, onOpen }: CatCardProps) { - const { user, updateUser } = useAuth(); + const { user } = useAuth(); const [liked, setLiked] = useState(false); const [likeCount, setLikeCount] = useState(cat.likes_count); const { toast } = useToast(); @@ -25,11 +25,8 @@ export default function CatCard({ cat, onOpen }: CatCardProps) { if (liked) { setLiked(false); setLikeCount((c) => c - 1); - try { - const res = await unlikeMutation.mutateAsync(); - if (res.ownerPoints !== undefined && cat.user_id === user.id) - updateUser({ ...user, points: res.ownerPoints }); - } catch { setLiked(true); setLikeCount((c) => c + 1); } + try { await unlikeMutation.mutateAsync(); } + catch { setLiked(true); setLikeCount((c) => c + 1); } } else { setLiked(true); setLikeCount((c) => c + 1); diff --git a/client/src/components/CatModal.tsx b/client/src/components/CatModal.tsx index cf8ac07..d25902c 100644 --- a/client/src/components/CatModal.tsx +++ b/client/src/components/CatModal.tsx @@ -12,7 +12,7 @@ interface CatModalProps { } export default function CatModal({ catId, onClose }: CatModalProps) { - const { user, updateUser } = useAuth(); + const { user } = useAuth(); const { data, isLoading } = useCat(catId); const { data: likeData, refetch: refetchLike } = useLikeStatus(catId); const likeMutation = useLikeCat(catId); @@ -40,11 +40,8 @@ export default function CatModal({ catId, onClose }: CatModalProps) { if (isOwner || !user || !cat) return; if (liked) { setLiked(false); setLikeCount((c) => c - 1); - try { - const res = await unlikeMutation.mutateAsync(); - if (res.ownerPoints !== undefined && cat.user_id === user.id) updateUser({ ...user, points: res.ownerPoints }); - refetchLike(); - } catch { setLiked(true); setLikeCount((c) => c + 1); } + try { await unlikeMutation.mutateAsync(); refetchLike(); } + catch { setLiked(true); setLikeCount((c) => c + 1); } } else { setLiked(true); setLikeCount((c) => c + 1); toast('Нравится ❤️'); diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index c07bedb..1cfa341 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -1,6 +1,7 @@ import { Link, useLocation } from 'react-router-dom'; import { useAuth } from '@/hooks/useAuth'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { Shield } from 'lucide-react'; export default function Navbar() { const { user } = useAuth(); @@ -19,12 +20,17 @@ export default function Navbar() { - + {user.username.charAt(0).toUpperCase()} + {user.is_admin && ( + + + + )} diff --git a/client/src/hooks/useLikes.ts b/client/src/hooks/useLikes.ts index bb7aa5f..7514d57 100644 --- a/client/src/hooks/useLikes.ts +++ b/client/src/hooks/useLikes.ts @@ -13,34 +13,26 @@ export function useLikeStatus(catId: number) { export function useLikeCat(catId: number) { const qc = useQueryClient(); - const { updateUser, user } = useAuth(); return useMutation({ mutationFn: () => likeCat(catId).then((r) => r.data), - onSuccess: (data) => { + onSuccess: () => { qc.invalidateQueries({ queryKey: ['likeStatus', catId] }); qc.invalidateQueries({ queryKey: ['cat', catId] }); qc.invalidateQueries({ queryKey: ['cats'] }); - if (user && data.ownerPoints !== undefined) { - updateUser({ ...user, points: data.ownerPoints }); - } }, }); } export function useUnlikeCat(catId: number) { const qc = useQueryClient(); - const { updateUser, user } = useAuth(); return useMutation({ mutationFn: () => unlikeCat(catId).then((r) => r.data), - onSuccess: (data) => { + onSuccess: () => { qc.invalidateQueries({ queryKey: ['likeStatus', catId] }); qc.invalidateQueries({ queryKey: ['cat', catId] }); qc.invalidateQueries({ queryKey: ['cats'] }); - if (user && data.ownerPoints !== undefined) { - updateUser({ ...user, points: data.ownerPoints }); - } }, }); -} +} \ No newline at end of file diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts index 093f344..ede3b12 100644 --- a/client/src/lib/auth.ts +++ b/client/src/lib/auth.ts @@ -5,6 +5,7 @@ export interface User { id: number; username: string; points: number; + is_admin?: boolean; created_at?: string; } diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx new file mode 100644 index 0000000..ba58118 --- /dev/null +++ b/client/src/pages/AdminPage.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '@/hooks/useAuth'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getAdminUsers, adminDeleteUser, adminDeleteCat } from '@/api/endpoints'; +import { Button } from '@/components/ui/button'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { ArrowLeft, Trash2, Shield } from 'lucide-react'; +import { useToast } from '@/components/Toast'; + +export default function AdminPage() { + const { user } = useAuth(); + const navigate = useNavigate(); + const { toast } = useToast(); + const qc = useQueryClient(); + + const { data, isLoading } = useQuery({ + queryKey: ['admin', 'users'], + queryFn: () => getAdminUsers().then(r => r.data), + enabled: !!user?.is_admin, + }); + + const deleteUserMutation = useMutation({ + mutationFn: (id: number) => adminDeleteUser(id), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['admin', 'users'] }); toast('Пользователь удалён'); }, + onError: () => toast('Ошибка удаления', 'error'), + }); + + if (!user?.is_admin) { + return ( +
+
+ +
+

Доступ запрещён

+ +
+ ); + } + + const users = data?.users ?? []; + + return ( +
+ + +
+ +

Админ-панель

+
+ + {isLoading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ ) : ( +
+
+ Пользователи ({users.length}) +
+ {users.map((u: any) => ( +
+ + {u.username.charAt(0).toUpperCase()} + +
+
+ @{u.username} + {u.is_admin ? ( + Админ + ) : null} +
+

🏆 {u.points} баллов

+
+ {!u.is_admin && ( + + )} +
+ ))} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/client/src/pages/CatPage.tsx b/client/src/pages/CatPage.tsx index 37763e4..f7951e8 100644 --- a/client/src/pages/CatPage.tsx +++ b/client/src/pages/CatPage.tsx @@ -13,7 +13,7 @@ export default function CatPage() { const { id } = useParams<{ id: string }>(); const catId = parseInt(id!); const navigate = useNavigate(); - const { user, updateUser } = useAuth(); + const { user } = useAuth(); const { toast } = useToast(); const { data, isLoading, isError } = useCat(catId); @@ -34,11 +34,8 @@ export default function CatPage() { if (isOwner || !user || !cat) return; if (isLiked) { setLiked(false); - try { - const res = await unlikeMutation.mutateAsync(); - if (res.ownerPoints !== undefined && cat.user_id === user.id) updateUser({ ...user, points: res.ownerPoints }); - refetchLike(); - } catch { setLiked(true); } + try { await unlikeMutation.mutateAsync(); refetchLike(); } + catch { setLiked(true); } } else { setLiked(true); toast('Нравится ❤️'); diff --git a/client/src/pages/ProfilePage.tsx b/client/src/pages/ProfilePage.tsx index ad4209d..56cae7f 100644 --- a/client/src/pages/ProfilePage.tsx +++ b/client/src/pages/ProfilePage.tsx @@ -6,7 +6,7 @@ import CatModal from '@/components/CatModal'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { ArrowLeft, Plus, LogOut, Heart } from 'lucide-react'; +import { ArrowLeft, Plus, LogOut, Heart, Shield } from 'lucide-react'; export default function ProfilePage() { const { id: paramId } = useParams<{ id?: string }>(); @@ -75,11 +75,18 @@ export default function ProfilePage() {

@{profileUser?.username}

- {isMe && ( - - )} +
+ {isMe && me?.is_admin && ( + + + + )} + {isMe && ( + + )} +
diff --git a/client/src/pages/UploadPage.tsx b/client/src/pages/UploadPage.tsx index ab6df01..049a267 100644 --- a/client/src/pages/UploadPage.tsx +++ b/client/src/pages/UploadPage.tsx @@ -36,7 +36,7 @@ export default function UploadPage() { try { const result = await uploadMutation.mutateAsync(fd); if (user) updateUser({ ...user, points: (user.points || 0) + 10 }); - toast('+10 баллов 🎉'); + toast('+1 балл 🎉'); navigate(`/cat/${result.cat.id}`); } catch { toast('Ошибка загрузки', 'error'); } }; @@ -98,7 +98,7 @@ export default function UploadPage() {
🏆 - +10 баллов + +1 балл за публикацию