mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-05 03:02:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormErrorMessage,
|
||||
Heading,
|
||||
Input,
|
||||
Text,
|
||||
Textarea,
|
||||
VStack,
|
||||
useToast,
|
||||
useColorModeValue,
|
||||
Container,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
SimpleGrid,
|
||||
HStack,
|
||||
Avatar,
|
||||
Badge,
|
||||
Icon,
|
||||
Link,
|
||||
Divider
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { sendContact } from '../services/public';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import MainLayout from '../components/layout/MainLayout';
|
||||
import { FiMail, FiPhone } from 'react-icons/fi';
|
||||
import { trackContactSubmit, trackFormSubmit } from '../utils/umami';
|
||||
import SponsorsSection from '../components/common/SponsorsSection';
|
||||
import ContactMap from '../components/home/ContactMap';
|
||||
import { getPublicContacts, GroupedContacts } from '../services/contactInfo';
|
||||
|
||||
type ContactFormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
const ContactPage: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const { settings } = useSettings();
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const cardBorder = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgColor = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
// Public contacts (grouped by category)
|
||||
const [contactsData, setContactsData] = useState<GroupedContacts | null>(null);
|
||||
const [contactsLoading, setContactsLoading] = useState(true);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<ContactFormData>();
|
||||
|
||||
const { mutate, isLoading } = useMutation({
|
||||
mutationFn: (data: ContactFormData) => sendContact({ ...data, source: 'contact' }),
|
||||
onSuccess: () => {
|
||||
reset();
|
||||
// Track successful contact form submission
|
||||
trackContactSubmit(true);
|
||||
trackFormSubmit('Contact Form', true);
|
||||
toast({
|
||||
title: 'Zpráva odeslána',
|
||||
description: 'Děkujeme za vaši zprávu. Brzy se vám ozveme zpět.',
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
onError: (error: any) => {
|
||||
// Heuristics for Axios errors
|
||||
const code = error?.code || '';
|
||||
const msgFromServer = error?.response?.data?.error || error?.response?.data?.message;
|
||||
const isTimeout = code === 'ECONNABORTED' || /timeout/i.test(String(error?.message || ''));
|
||||
const isNetwork = !!error?.isAxiosError && !error?.response;
|
||||
|
||||
const description = msgFromServer
|
||||
|| (isTimeout ? 'Vypršel časový limit požadavku. Zkuste to prosím znovu za chvíli.'
|
||||
: isNetwork ? 'Požadavek se nezdařil (síť/CORS). Zkuste to znovu nebo obnovte stránku.'
|
||||
: 'Něco se pokazilo. Zkuste to prosím znovu později.');
|
||||
|
||||
// Track failed contact form submission
|
||||
trackContactSubmit(false);
|
||||
trackFormSubmit('Contact Form', false);
|
||||
|
||||
toast({
|
||||
title: 'Chyba',
|
||||
description,
|
||||
status: 'error',
|
||||
duration: 6000,
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Load public contacts once
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
const data = await getPublicContacts();
|
||||
if (mounted) setContactsData(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to load contacts', e);
|
||||
} finally {
|
||||
if (mounted) setContactsLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
|
||||
const onSubmit = (data: ContactFormData) => {
|
||||
mutate(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<Container maxW="container.xl">
|
||||
<VStack align="stretch" spacing={8}>
|
||||
{/* Top: Map + Contact categories (tabs) */}
|
||||
<Box>
|
||||
<Heading size="xl" mb={4}>Kontakt</Heading>
|
||||
{/* contacts are loaded in useEffect */}
|
||||
|
||||
{(() => {
|
||||
const lat = (settings as any)?.location_latitude;
|
||||
const lng = (settings as any)?.location_longitude;
|
||||
const hasLocation = !!lat && !!lng;
|
||||
const categories = Object.entries(contactsData?.categories || {});
|
||||
const uncategorized = contactsData?.uncategorized || [];
|
||||
const hasContacts = categories.length > 0 || uncategorized.length > 0;
|
||||
|
||||
if (!hasLocation && !hasContacts) return null;
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={8}>
|
||||
{/* Map on the left */}
|
||||
{hasLocation && (
|
||||
<Box borderRadius="lg" overflow="hidden" boxShadow="md">
|
||||
<ContactMap
|
||||
latitude={lat}
|
||||
longitude={lng}
|
||||
zoom={(settings as any)?.map_zoom_level || 15}
|
||||
address={(settings as any)?.contact_address}
|
||||
clubName={(settings as any)?.club_name || (settings as any)?.site_title}
|
||||
mapStyle={(settings as any)?.map_style || 'default'}
|
||||
clubPrimaryColor={(settings as any)?.primary_color}
|
||||
clubSecondaryColor={(settings as any)?.accent_color}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Categories as tabs on the right */}
|
||||
{hasContacts && (
|
||||
<Box bg={bgColor} p={4} borderRadius="lg" borderWidth="1px" borderColor={borderColor} boxShadow="sm">
|
||||
<Heading size="md" mb={3}>Kontaktní osoby</Heading>
|
||||
<Tabs colorScheme="blue" isFitted>
|
||||
<TabList>
|
||||
{categories.map(([name]) => (
|
||||
<Tab key={name}>{name}</Tab>
|
||||
))}
|
||||
{uncategorized.length > 0 && <Tab>Ostatní</Tab>}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{categories.map(([name, persons]) => (
|
||||
<TabPanel key={name} pt={4}>
|
||||
<SimpleGrid columns={{ base: 1, sm: 2 }} spacing={4}>
|
||||
{persons.map((contact) => (
|
||||
<Box key={contact.id} bg={bgColor} p={4} borderRadius="md" borderWidth="1px" borderColor={borderColor}>
|
||||
<VStack align="start" spacing={3}>
|
||||
{contact.image_url && (
|
||||
<Avatar src={contact.image_url} name={contact.name} size="lg" />
|
||||
)}
|
||||
<Box>
|
||||
<Heading size="sm">{contact.name}</Heading>
|
||||
{contact.position && (
|
||||
<Badge colorScheme="blue" mt={1}>{contact.position}</Badge>
|
||||
)}
|
||||
</Box>
|
||||
{contact.description && (
|
||||
<Text fontSize="sm" color="gray.600">{contact.description}</Text>
|
||||
)}
|
||||
<VStack align="start" spacing={1}>
|
||||
{contact.email && (
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FiMail} color="blue.500" />
|
||||
<Link href={`mailto:${contact.email}`} color="blue.500" fontSize="sm">{contact.email}</Link>
|
||||
</HStack>
|
||||
)}
|
||||
{contact.phone && (
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FiPhone} color="blue.500" />
|
||||
<Link href={`tel:${contact.phone}`} color="blue.500" fontSize="sm">{contact.phone}</Link>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</TabPanel>
|
||||
))}
|
||||
{uncategorized.length > 0 && (
|
||||
<TabPanel pt={4}>
|
||||
<SimpleGrid columns={{ base: 1, sm: 2 }} spacing={4}>
|
||||
{uncategorized.map((contact) => (
|
||||
<Box key={contact.id} bg={bgColor} p={4} borderRadius="md" borderWidth="1px" borderColor={borderColor}>
|
||||
<VStack align="start" spacing={3}>
|
||||
{contact.image_url && (
|
||||
<Avatar src={contact.image_url} name={contact.name} size="lg" />
|
||||
)}
|
||||
<Box>
|
||||
<Heading size="sm">{contact.name}</Heading>
|
||||
{contact.position && (
|
||||
<Badge colorScheme="blue" mt={1}>{contact.position}</Badge>
|
||||
)}
|
||||
</Box>
|
||||
{contact.description && (
|
||||
<Text fontSize="sm" color="gray.600">{contact.description}</Text>
|
||||
)}
|
||||
<VStack align="start" spacing={1}>
|
||||
{contact.email && (
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FiMail} color="blue.500" />
|
||||
<Link href={`mailto:${contact.email}`} color="blue.500" fontSize="sm">{contact.email}</Link>
|
||||
</HStack>
|
||||
)}
|
||||
{contact.phone && (
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FiPhone} color="blue.500" />
|
||||
<Link href={`tel:${contact.phone}`} color="blue.500" fontSize="sm">{contact.phone}</Link>
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Contact form at the end */}
|
||||
<Box>
|
||||
<Heading size="lg" mb={2} color={settings?.primaryColor || 'brand.500'}>
|
||||
Kontaktujte nás
|
||||
</Heading>
|
||||
<Text color="gray.500">
|
||||
Máte dotaz nebo připomínku? Napište nám zprávu a my se vám ozveme co nejdříve zpět.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
bg={cardBg}
|
||||
borderRadius="lg"
|
||||
borderWidth="1px"
|
||||
borderColor={cardBorder}
|
||||
boxShadow="sm"
|
||||
p={6}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<VStack spacing={4}>
|
||||
<FormControl isInvalid={!!errors.name}>
|
||||
<FormLabel htmlFor="name">Jméno a příjmení *</FormLabel>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="Jan Novák"
|
||||
{...register('name', {
|
||||
required: 'Toto pole je povinné',
|
||||
minLength: { value: 2, message: 'Jméno musí mít alespoň 2 znaky' },
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.name && errors.name.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<FormControl isInvalid={!!errors.email}>
|
||||
<FormLabel htmlFor="email">E-mailová adresa *</FormLabel>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="vas@email.cz"
|
||||
{...register('email', {
|
||||
required: 'Toto pole je povinné',
|
||||
pattern: {
|
||||
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
message: 'Neplatná e-mailová adresa',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.email && errors.email.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<FormControl isInvalid={!!errors.subject}>
|
||||
<FormLabel htmlFor="subject">Předmět</FormLabel>
|
||||
<Input
|
||||
id="subject"
|
||||
placeholder="Předmět zprávy"
|
||||
{...register('subject', {
|
||||
required: 'Předmět je povinný',
|
||||
maxLength: { value: 100, message: 'Předmět může mít maximálně 100 znaků' },
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.subject && errors.subject.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<FormControl isInvalid={!!errors.message}>
|
||||
<FormLabel htmlFor="message">Zpráva *</FormLabel>
|
||||
<Textarea
|
||||
id="message"
|
||||
rows={6}
|
||||
placeholder="Napište nám zprávu..."
|
||||
{...register('message', {
|
||||
required: 'Toto pole je povinné',
|
||||
minLength: { value: 10, message: 'Zpráva musí mít alespoň 10 znaků' },
|
||||
maxLength: { value: 2000, message: 'Zpráva může mít maximálně 2000 znaků' },
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.message && errors.message.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme={settings?.primaryColor || 'brand'}
|
||||
size="lg"
|
||||
width="full"
|
||||
mt={4}
|
||||
isLoading={isLoading}
|
||||
loadingText="Odesílám..."
|
||||
data-umami-event="Contact Form Submit"
|
||||
>
|
||||
Odeslat zprávu
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Container>
|
||||
|
||||
{/* Sponsors Section */}
|
||||
<SponsorsSection />
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactPage;
|
||||
Reference in New Issue
Block a user