Files
MyClub/frontend/src/pages/admin/CommentsAdminPage.tsx
T
Tomas Dvorak b9cea0cd77 dev day #79
2025-11-02 01:04:02 +01:00

179 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import AdminLayout from '../../layouts/AdminLayout';
import { Box, Heading, HStack, VStack, Button, Select, Input, Table, Thead, Tbody, Tr, Th, Td, Text, Badge, IconButton, useToast, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalCloseButton, useDisclosure, FormControl, FormLabel, NumberInput, NumberInputField } from '@chakra-ui/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { adminListComments, adminUpdateCommentStatus, adminBanUser, adminListUnbanRequests, adminResolveUnban } from '../../services/admin/comments';
import { deleteComment } from '../../services/comments';
import { FiTrash2 } from 'react-icons/fi';
const CommentsAdminPage: React.FC = () => {
const [status, setStatus] = React.useState<string>('');
const [targetType, setTargetType] = React.useState<string>('');
const [targetId, setTargetId] = React.useState<string>('');
const [userId, setUserId] = React.useState<string>('');
const [page, setPage] = React.useState<number>(1);
const toast = useToast();
const qc = useQueryClient();
const listQ = useQuery({
queryKey: ['admin-comments', { status, targetType, targetId, userId, page }],
queryFn: () => adminListComments({ status: status as any, target_type: targetType, target_id: targetId, user_id: userId, page, page_size: 50 }),
keepPreviousData: true,
});
const unbanQ = useQuery({
queryKey: ['admin-unban-requests'],
queryFn: adminListUnbanRequests,
});
const updateStatusMut = useMutation({
mutationFn: (args: { id: number; s: 'visible'|'hidden' }) => adminUpdateCommentStatus(args.id, args.s),
onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['admin-comments'] }); },
});
const deleteMut = useMutation({
mutationFn: (id: number) => deleteComment(id),
onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['admin-comments'] }); toast({ status: 'success', title: 'Smazáno' }); },
});
const [banUserId, setBanUserId] = React.useState<number | null>(null);
const banModal = useDisclosure();
const [banReason, setBanReason] = React.useState<string>('Porušení pravidel diskuse');
const [banHours, setBanHours] = React.useState<number>(0);
const banMut = useMutation({
mutationFn: () => adminBanUser(banUserId || 0, banReason, banHours),
onSuccess: async () => { banModal.onClose(); setBanUserId(null); toast({ status: 'success', title: 'Uživatel zablokován' }); },
});
const resolveUnbanMut = useMutation({
mutationFn: (args: { id: number; action: 'approve'|'reject' }) => adminResolveUnban(args.id, args.action),
onSuccess: async () => { await qc.invalidateQueries({ queryKey: ['admin-unban-requests'] }); toast({ status: 'success', title: 'Vyřízeno' }); },
});
const items = listQ.data?.items || [];
return (
<AdminLayout>
<Box>
<Heading size="md" mb={4}>Komentáře (moderace)</Heading>
<VStack align="stretch" spacing={3} mb={4}>
<HStack>
<Select placeholder="Status" value={status} onChange={(e) => { setStatus(e.target.value); setPage(1); }} maxW="200px">
<option value="visible">Viditelné</option>
<option value="hidden">Skryté</option>
</Select>
<Select placeholder="Typ cíle" value={targetType} onChange={(e) => { setTargetType(e.target.value); setPage(1); }} maxW="220px">
<option value="article">Článek</option>
<option value="event">Aktivita</option>
<option value="gallery_album">Galerie</option>
<option value="youtube_video">YouTube video</option>
</Select>
<Input placeholder="Target ID" value={targetId} onChange={(e) => { setTargetId(e.target.value); setPage(1); }} maxW="200px" />
<Input placeholder="User ID" value={userId} onChange={(e) => { setUserId(e.target.value); setPage(1); }} maxW="200px" />
</HStack>
</VStack>
<Box borderWidth="1px" borderRadius="md" overflowX="auto">
<Table size="sm">
<Thead>
<Tr>
<Th>ID</Th>
<Th>Uživatel</Th>
<Th>Cíl</Th>
<Th>Obsah</Th>
<Th>Spam</Th>
<Th>Status</Th>
<Th>Akce</Th>
</Tr>
</Thead>
<Tbody>
{items.map((c) => (
<Tr key={c.id}>
<Td>#{c.id}</Td>
<Td>#{c.user?.id} {c.user?.first_name} {c.user?.last_name}</Td>
<Td><Badge>{c.target_type}</Badge> <Text as="span">{c.target_id}</Text></Td>
<Td maxW="420px"><Text noOfLines={2}>{c.content}</Text></Td>
<Td>{(c as any).spam_score ? <Badge colorScheme={(c as any).spam_score > 0.5 ? 'orange' : 'green'}>{(c as any).spam_score.toFixed(2)}</Badge> : '-'}</Td>
<Td>
<HStack>
<Button size="xs" variant={c.status === 'visible' ? 'solid' : 'outline'} onClick={() => updateStatusMut.mutate({ id: c.id, s: 'visible' })}>Viditelné</Button>
<Button size="xs" variant={c.status === 'hidden' ? 'solid' : 'outline'} onClick={() => updateStatusMut.mutate({ id: c.id, s: 'hidden' })}>Skryté</Button>
</HStack>
</Td>
<Td>
<HStack>
<IconButton aria-label="Smazat" size="xs" icon={<FiTrash2 />} onClick={() => deleteMut.mutate(c.id)} />
<Button size="xs" variant="outline" onClick={() => { setBanUserId(c.user?.id as any); banModal.onOpen(); }}>Ban</Button>
</HStack>
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
<Heading size="sm" mt={6} mb={2}>Žádosti o odblokování</Heading>
<Box borderWidth="1px" borderRadius="md" overflowX="auto">
<Table size="sm">
<Thead>
<Tr>
<Th>ID</Th>
<Th>Uživatel</Th>
<Th>Text</Th>
<Th>Status</Th>
<Th>Akce</Th>
</Tr>
</Thead>
<Tbody>
{(unbanQ.data?.items || []).map((r) => (
<Tr key={r.id}>
<Td>#{r.id}</Td>
<Td>#{r.user_id}</Td>
<Td maxW="480px"><Text noOfLines={2}>{r.message}</Text></Td>
<Td><Badge>{r.status}</Badge></Td>
<Td>
<HStack>
<Button size="xs" colorScheme="green" variant="outline" onClick={() => resolveUnbanMut.mutate({ id: r.id, action: 'approve' })}>Povolit</Button>
<Button size="xs" colorScheme="red" variant="outline" onClick={() => resolveUnbanMut.mutate({ id: r.id, action: 'reject' })}>Zamítnout</Button>
</HStack>
</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
{/* Ban modal */}
<Modal isOpen={banModal.isOpen} onClose={banModal.onClose} isCentered>
<ModalOverlay />
<ModalContent>
<ModalHeader>Zablokovat uživatele #{banUserId}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack align="stretch" spacing={3}>
<FormControl>
<FormLabel>Důvod</FormLabel>
<Input value={banReason} onChange={(e) => setBanReason(e.target.value)} />
</FormControl>
<FormControl>
<FormLabel>Doba (hodiny) 0 = trvale</FormLabel>
<NumberInput min={0} value={banHours} onChange={(v) => setBanHours(Number(v) || 0)}>
<NumberInputField />
</NumberInput>
</FormControl>
</VStack>
</ModalBody>
<ModalFooter>
<HStack>
<Button onClick={banModal.onClose}>Zrušit</Button>
<Button colorScheme="red" isLoading={banMut.isPending} onClick={() => banMut.mutate()}>Zablokovat</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
</AdminLayout>
);
};
export default CommentsAdminPage;