mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #65
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user