This commit is contained in:
Tomas Dvorak
2025-10-19 17:16:57 +02:00
parent e9a63073e5
commit 77213f4e83
76 changed files with 9728 additions and 935 deletions
+294 -55
View File
@@ -17,9 +17,18 @@ import {
useColorModeValue,
Collapse,
Divider,
Input,
Textarea,
Switch,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
} from '@chakra-ui/react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { AddIcon, DeleteIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
import { AddIcon, DeleteIcon, ChevronDownIcon, ChevronUpIcon, CloseIcon } from '@chakra-ui/icons';
import { FiPlus } from 'react-icons/fi';
import { getPolls, createPoll, updatePoll, Poll, CreatePollRequest } from '../../services/polls';
interface PollLinkerProps {
@@ -37,6 +46,25 @@ const PollLinker: React.FC<PollLinkerProps> = ({ articleId, eventId, onPollsChan
const queryClient = useQueryClient();
const [isExpanded, setIsExpanded] = useState(false);
const [selectedPollId, setSelectedPollId] = useState<string>('');
const [showCreateForm, setShowCreateForm] = useState(false);
// Poll creation form state
const [newPollData, setNewPollData] = useState<CreatePollRequest>({
title: '',
description: '',
type: 'single',
status: 'active',
allow_multiple: false,
max_choices: 1,
show_results: 'after_vote',
require_auth: false,
allow_guest_vote: true,
featured: false,
options: [
{ text: '', display_order: 0 },
{ text: '', display_order: 1 },
],
});
const bgBox = useColorModeValue('gray.50', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
@@ -115,6 +143,38 @@ const PollLinker: React.FC<PollLinkerProps> = ({ articleId, eventId, onPollsChan
},
});
// Mutation to create new poll
const createPollMutation = useMutation({
mutationFn: async (pollData: CreatePollRequest) => {
// Add relation to article or event
const createData = { ...pollData };
if (articleId) createData.related_article_id = articleId;
if (eventId) createData.related_event_id = eventId;
return createPoll(createData);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['linked-polls'] });
queryClient.invalidateQueries({ queryKey: ['all-admin-polls'] });
toast({
title: 'Anketa vytvořena a propojena',
status: 'success',
duration: 3000,
});
setShowCreateForm(false);
resetNewPollForm();
if (onPollsChanged) onPollsChanged();
},
onError: (error: any) => {
toast({
title: 'Chyba při vytváření ankety',
description: error.response?.data?.error || 'Nepodařilo se vytvořit anketu',
status: 'error',
duration: 5000,
});
},
});
const handleLinkPoll = () => {
if (!selectedPollId) {
toast({
@@ -134,6 +194,82 @@ const PollLinker: React.FC<PollLinkerProps> = ({ articleId, eventId, onPollsChan
}
};
const resetNewPollForm = () => {
setNewPollData({
title: '',
description: '',
type: 'single',
status: 'active',
allow_multiple: false,
max_choices: 1,
show_results: 'after_vote',
require_auth: false,
allow_guest_vote: true,
featured: false,
options: [
{ text: '', display_order: 0 },
{ text: '', display_order: 1 },
],
});
};
const handleCreatePoll = () => {
// Validate form
if (!newPollData.title.trim()) {
toast({
title: 'Chybí název ankety',
status: 'warning',
duration: 3000,
});
return;
}
const validOptions = newPollData.options.filter(opt => opt.text.trim());
if (validOptions.length < 2) {
toast({
title: 'Anketa musí mít alespoň 2 možnosti',
status: 'warning',
duration: 3000,
});
return;
}
// Submit with only valid options
createPollMutation.mutate({
...newPollData,
options: validOptions,
});
};
const addOption = () => {
setNewPollData(prev => ({
...prev,
options: [...prev.options, { text: '', display_order: prev.options.length }],
}));
};
const removeOption = (index: number) => {
if (newPollData.options.length <= 2) {
toast({
title: 'Anketa musí mít alespoň 2 možnosti',
status: 'warning',
duration: 2000,
});
return;
}
setNewPollData(prev => ({
...prev,
options: prev.options.filter((_, i) => i !== index).map((opt, idx) => ({ ...opt, display_order: idx })),
}));
};
const updateOption = (index: number, text: string) => {
setNewPollData(prev => ({
...prev,
options: prev.options.map((opt, i) => i === index ? { ...opt, text } : opt),
}));
};
// Filter out polls that are already linked
const linkedPollIds = new Set(linkedPolls?.map(p => p.id) || []);
const availablePolls = allPolls?.filter(p => !linkedPollIds.has(p.id)) || [];
@@ -225,61 +361,164 @@ const PollLinker: React.FC<PollLinkerProps> = ({ articleId, eventId, onPollsChan
<Divider />
{isLoadingAll ? (
<HStack justify="center" py={4}>
<Spinner size="sm" />
</HStack>
) : availablePolls.length > 0 ? (
<VStack spacing={3} align="stretch">
<Text fontSize="xs" fontWeight="bold" color="gray.500">
Připojit existující anketu:
</Text>
<HStack>
<Select
value={selectedPollId}
onChange={(e) => setSelectedPollId(e.target.value)}
placeholder="Vyberte anketu..."
size="sm"
flex={1}
>
{availablePolls.map((poll) => (
<option key={poll.id} value={poll.id}>
{poll.title} ({poll.status}) - {poll.total_votes} hlasů
</option>
))}
</Select>
<Button
leftIcon={<AddIcon />}
onClick={handleLinkPoll}
size="sm"
colorScheme="blue"
isLoading={linkPollMutation.isPending}
isDisabled={!selectedPollId}
>
Připojit
</Button>
</HStack>
</VStack>
) : (
<Alert status="warning" size="sm">
<AlertIcon />
<Text fontSize="sm">
Žádné dostupné ankety. Vytvořte novou v{' '}
<Button
as="a"
href="/admin/ankety"
target="_blank"
variant="link"
size="sm"
colorScheme="blue"
>
správě anket
</Button>
</Text>
</Alert>
)}
<Tabs size="sm" variant="enclosed">
<TabList>
<Tab>Propojit existující</Tab>
<Tab>Vytvořit novou</Tab>
</TabList>
<Text fontSize="xs" color="gray.500">
<TabPanels>
{/* Tab 1: Link existing poll */}
<TabPanel px={0} py={3}>
{isLoadingAll ? (
<HStack justify="center" py={4}>
<Spinner size="sm" />
</HStack>
) : availablePolls.length > 0 ? (
<VStack spacing={3} align="stretch">
<HStack>
<Select
value={selectedPollId}
onChange={(e) => setSelectedPollId(e.target.value)}
placeholder="Vyberte anketu..."
size="sm"
flex={1}
>
{availablePolls.map((poll) => (
<option key={poll.id} value={poll.id}>
{poll.title} ({poll.status}) - {poll.total_votes} hlasů
</option>
))}
</Select>
<Button
leftIcon={<AddIcon />}
onClick={handleLinkPoll}
size="sm"
colorScheme="blue"
isLoading={linkPollMutation.isPending}
isDisabled={!selectedPollId}
>
Připojit
</Button>
</HStack>
</VStack>
) : (
<Alert status="info" size="sm">
<AlertIcon />
<Text fontSize="sm">Žádné dostupné ankety. Vytvořte novou v druhé záložce.</Text>
</Alert>
)}
</TabPanel>
{/* Tab 2: Create new poll */}
<TabPanel px={0} py={3}>
<VStack spacing={3} align="stretch">
<FormControl isRequired>
<FormLabel fontSize="sm">Název ankety</FormLabel>
<Input
size="sm"
placeholder="např. Dorazíš na příští trénink?"
value={newPollData.title}
onChange={(e) => setNewPollData(prev => ({ ...prev, title: e.target.value }))}
/>
</FormControl>
<FormControl>
<FormLabel fontSize="sm">Popis (volitelné)</FormLabel>
<Textarea
size="sm"
placeholder="Doplňující informace k anketě..."
rows={2}
value={newPollData.description}
onChange={(e) => setNewPollData(prev => ({ ...prev, description: e.target.value }))}
/>
</FormControl>
<FormControl>
<FormLabel fontSize="sm">Typ ankety</FormLabel>
<Select
size="sm"
value={newPollData.type}
onChange={(e) => setNewPollData(prev => ({ ...prev, type: e.target.value as any }))}
>
<option value="single">Jedna odpověď</option>
<option value="multiple">Více odpovědí</option>
</Select>
</FormControl>
<FormControl>
<FormLabel fontSize="sm">Možnosti (min. 2)</FormLabel>
<VStack spacing={2} align="stretch">
{newPollData.options.map((option, index) => (
<HStack key={index}>
<Input
size="sm"
placeholder={`Možnost ${index + 1}`}
value={option.text}
onChange={(e) => updateOption(index, e.target.value)}
/>
{newPollData.options.length > 2 && (
<IconButton
aria-label="Odebrat možnost"
icon={<CloseIcon />}
size="sm"
variant="ghost"
colorScheme="red"
onClick={() => removeOption(index)}
/>
)}
</HStack>
))}
<Button
size="sm"
variant="outline"
leftIcon={<AddIcon />}
onClick={addOption}
>
Přidat možnost
</Button>
</VStack>
</FormControl>
<HStack spacing={4}>
<FormControl display="flex" alignItems="center">
<FormLabel fontSize="sm" mb="0" mr={2}>
Povolit hlasování hostů
</FormLabel>
<Switch
size="sm"
isChecked={newPollData.allow_guest_vote}
onChange={(e) => setNewPollData(prev => ({ ...prev, allow_guest_vote: e.target.checked }))}
/>
</FormControl>
<FormControl display="flex" alignItems="center">
<FormLabel fontSize="sm" mb="0" mr={2}>
Aktivní
</FormLabel>
<Switch
size="sm"
isChecked={newPollData.status === 'active'}
onChange={(e) => setNewPollData(prev => ({ ...prev, status: e.target.checked ? 'active' : 'draft' }))}
/>
</FormControl>
</HStack>
<Button
colorScheme="green"
size="sm"
leftIcon={<AddIcon />}
onClick={handleCreatePoll}
isLoading={createPollMutation.isPending}
>
Vytvořit anketu
</Button>
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
<Text fontSize="xs" color="gray.500" mt={2}>
💡 Tip: Ankety se zobrazí automaticky na konci {articleId ? 'článku' : 'aktivity'}
</Text>
</VStack>