mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
371 lines
11 KiB
TypeScript
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 až {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;
|