Files
MyClub/frontend/src/components/polls/PollCard.tsx
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

371 lines
11 KiB
TypeScript

import React, { useState } from 'react';
import {
Box,
VStack,
HStack,
Text,
Button,
Radio,
RadioGroup,
Checkbox,
CheckboxGroup,
Progress,
Badge,
useToast,
Image,
Heading,
useColorModeValue,
} from '@chakra-ui/react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { CheckIcon } from '@chakra-ui/icons';
import {
Poll,
PollOption,
votePoll,
getPollResults,
generateSessionToken,
} from '../../services/polls';
interface PollCardProps {
poll: Poll;
hasVoted: boolean;
isActive: boolean;
canShowResults: boolean;
onVoteSuccess?: () => void;
}
const PollCard: React.FC<PollCardProps> = ({
poll,
hasVoted: initialHasVoted,
isActive,
canShowResults: initialCanShowResults,
onVoteSuccess,
}) => {
const toast = useToast();
const queryClient = useQueryClient();
const [selectedOptions, setSelectedOptions] = useState<number[]>([]);
const [hasVoted, setHasVoted] = useState(initialHasVoted);
const [canShowResults, setCanShowResults] = useState(initialCanShowResults);
const [results, setResults] = useState<any[]>([]);
const [showingResults, setShowingResults] = useState(initialCanShowResults);
const bgCard = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
// Vote mutation
const voteMutation = useMutation({
mutationFn: () => {
const sessionToken = generateSessionToken();
return votePoll(poll.id, {
option_ids: selectedOptions,
session_token: sessionToken,
});
},
onSuccess: async () => {
setHasVoted(true);
setCanShowResults(true);
setShowingResults(true);
// Fetch results
try {
const resultsData = await getPollResults(poll.id);
setResults(resultsData.results);
} catch (error) {
console.error('Failed to fetch results:', error);
}
queryClient.invalidateQueries({ queryKey: ['polls'] });
queryClient.invalidateQueries({ queryKey: ['poll', poll.id] });
toast({
title: 'Hlas zaznamenán!',
description: 'Děkujeme za vaši účast v anketě.',
status: 'success',
duration: 3000,
});
if (onVoteSuccess) {
onVoteSuccess();
}
},
onError: (error: any) => {
toast({
title: 'Chyba',
description: error.response?.data?.error || 'Nepodařilo se zaznamenat váš hlas',
status: 'error',
duration: 5000,
});
},
});
const handleVote = () => {
if (selectedOptions.length === 0) {
toast({
title: 'Vyberte možnost',
description: 'Před hlasováním vyberte alespoň jednu možnost.',
status: 'warning',
duration: 3000,
});
return;
}
if (poll.allow_multiple && selectedOptions.length > poll.max_choices) {
toast({
title: 'Příliš mnoho voleb',
description: `Můžete vybrat maximálně ${poll.max_choices} možností.`,
status: 'warning',
duration: 3000,
});
return;
}
voteMutation.mutate();
};
const handleSingleChoice = (value: string) => {
setSelectedOptions([parseInt(value)]);
};
const handleMultipleChoice = (values: (string | number)[]) => {
const numValues = values.map((v) => (typeof v === 'string' ? parseInt(v) : v));
if (numValues.length <= poll.max_choices) {
setSelectedOptions(numValues);
}
};
const loadResults = async () => {
try {
const resultsData = await getPollResults(poll.id);
setResults(resultsData.results);
setShowingResults(true);
} catch (error: any) {
toast({
title: 'Chyba',
description: 'Nepodařilo se načíst výsledky',
status: 'error',
duration: 3000,
});
}
};
const calculatePercentage = (voteCount: number) => {
if (poll.total_votes === 0) return 0;
return (voteCount / poll.total_votes) * 100;
};
// Show results if available
if (showingResults && canShowResults) {
const displayResults = results.length > 0 ? results : poll.options.map(opt => ({
option_id: opt.id,
text: opt.text,
vote_count: opt.vote_count,
percentage: calculatePercentage(opt.vote_count),
image_url: opt.image_url,
}));
return (
<Box
bg={bgCard}
borderWidth="1px"
borderColor={borderColor}
borderRadius="xl"
p={6}
boxShadow="md"
>
<VStack spacing={4} align="stretch">
{poll.image_url && (
<Image
src={poll.image_url}
alt={poll.title}
borderRadius="lg"
maxH="200px"
objectFit="cover"
/>
)}
<HStack justify="space-between" align="start">
<Heading size="md">{poll.title}</Heading>
{hasVoted && (
<Badge colorScheme="green" fontSize="sm">
<HStack spacing={1}>
<CheckIcon boxSize={3} />
<Text>Hlasováno</Text>
</HStack>
</Badge>
)}
</HStack>
{poll.description && (
<Text fontSize="sm" color="gray.600">
{poll.description}
</Text>
)}
<VStack spacing={3} align="stretch">
<Text fontWeight="bold" fontSize="sm" color="gray.500">
Výsledky ({poll.total_votes} hlasů)
</Text>
{displayResults.map((result) => (
<Box key={result.option_id}>
<HStack justify="space-between" mb={1}>
<Text fontWeight="medium">{result.text}</Text>
<Text fontSize="sm" color="gray.500">
{result.vote_count} ({result.percentage.toFixed(1)}%)
</Text>
</HStack>
<Progress
value={result.percentage}
colorScheme="blue"
borderRadius="full"
size="sm"
/>
</Box>
))}
</VStack>
</VStack>
</Box>
);
}
// Show voting form
return (
<Box
bg={bgCard}
borderWidth="1px"
borderColor={borderColor}
borderRadius="xl"
p={6}
boxShadow="md"
>
<VStack spacing={4} align="stretch">
{poll.image_url && (
<Image
src={poll.image_url}
alt={poll.title}
borderRadius="lg"
maxH="200px"
objectFit="cover"
/>
)}
<Heading size="md">{poll.title}</Heading>
{poll.description && (
<Text fontSize="sm" color="gray.600">
{poll.description}
</Text>
)}
{!isActive && (
<Badge colorScheme="orange">Anketa je momentálně uzavřena</Badge>
)}
{isActive && (
<>
{poll.allow_multiple ? (
<CheckboxGroup
value={selectedOptions.map(String)}
onChange={handleMultipleChoice}
>
<VStack spacing={2} align="stretch">
<Text fontSize="sm" color="gray.500">
Vyberte {poll.max_choices} možností
</Text>
{poll.options.map((option) => (
<Box
key={option.id}
p={3}
borderWidth="1px"
borderRadius="md"
_hover={{ bg: hoverBg }}
cursor="pointer"
>
<Checkbox value={String(option.id)}>
<VStack align="start" spacing={1}>
<Text>{option.text}</Text>
{option.description && (
<Text fontSize="xs" color="gray.500">
{option.description}
</Text>
)}
</VStack>
</Checkbox>
</Box>
))}
</VStack>
</CheckboxGroup>
) : (
<RadioGroup
value={selectedOptions[0]?.toString() || ''}
onChange={handleSingleChoice}
>
<VStack spacing={2} align="stretch">
{poll.options.map((option) => (
<Box
key={option.id}
p={3}
borderWidth="1px"
borderRadius="md"
_hover={{ bg: hoverBg }}
cursor="pointer"
>
<Radio value={String(option.id)}>
<VStack align="start" spacing={1}>
<Text>{option.text}</Text>
{option.description && (
<Text fontSize="xs" color="gray.500">
{option.description}
</Text>
)}
{option.player && (
<HStack spacing={2}>
{option.player.image_url && (
<Image
src={option.player.image_url}
alt={`${option.player.first_name} ${option.player.last_name}`}
boxSize="24px"
borderRadius="full"
/>
)}
<Text fontSize="xs" color="gray.500">
#{option.player.jersey_number} {option.player.first_name}{' '}
{option.player.last_name}
</Text>
</HStack>
)}
</VStack>
</Radio>
</Box>
))}
</VStack>
</RadioGroup>
)}
<Button
colorScheme="blue"
onClick={handleVote}
isLoading={voteMutation.isPending}
isDisabled={!isActive || selectedOptions.length === 0}
>
Hlasovat
</Button>
</>
)}
{canShowResults && !showingResults && (
<Button variant="outline" onClick={loadResults} size="sm">
Zobrazit výsledky
</Button>
)}
<Text fontSize="xs" color="gray.500" textAlign="center">
Celkem hlasů: {poll.total_votes}
</Text>
</VStack>
</Box>
);
};
export default PollCard;