mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user