Files
MyClub/frontend/src/pages/admin/CommentsAdminPage.tsx
T
Tomas Dvorak 087f30e82c dev day #80
2025-11-02 21:31:00 +01:00

204 lines
9.9 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, Switch } 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 [reportedOnly, setReportedOnly] = React.useState<boolean>(false);
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 itemsAll = listQ.data?.items || [];
const items = React.useMemo(() => {
if (!reportedOnly) return itemsAll;
return itemsAll.filter((c: any) => (c as any).reports && (c as any).reports > 0);
}, [itemsAll, reportedOnly]);
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>
<Text fontSize="sm" color="gray.500">Jen nahlášené</Text>
<Switch isChecked={reportedOnly} onChange={(e)=>setReportedOnly(e.target.checked)} />
</HStack>
</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>Hlášení</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>{(c as any).reports ? <Badge colorScheme={(c as any).reports > 2 ? 'red' : 'yellow'}>{(c as any).reports}</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>
<HStack mt={3} justify="space-between">
<Text fontSize="sm" color="gray.500">Stránka {page} {listQ.data?.total || 0} komentářů</Text>
<HStack>
<Button size="sm" variant="outline" onClick={() => setPage(p => Math.max(1, p - 1))} isDisabled={page <= 1}>Předchozí</Button>
<Button size="sm" variant="outline" onClick={() => setPage(p => p + 1)} isDisabled={(itemsAll.length === 0) || ((itemsAll.length < 50) && (listQ.data?.total || 0) <= (page * 50))}>Další</Button>
</HStack>
</HStack>
<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>
<HStack>
<Text fontSize="sm" color="gray.500">Rychlá volba:</Text>
<Button size="xs" variant="outline" onClick={()=>setBanHours(24)}>24h</Button>
<Button size="xs" variant="outline" onClick={()=>setBanHours(24*7)}>7 dní</Button>
<Button size="xs" variant="outline" onClick={()=>setBanHours(0)}>Trvale</Button>
</HStack>
</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;