This commit is contained in:
Tomas Dvorak
2025-11-02 01:04:02 +01:00
parent ac886502e0
commit b9cea0cd77
153 changed files with 43713 additions and 1700 deletions
+172 -88
View File
@@ -33,9 +33,10 @@ import { useSettings } from '../hooks/useSettings';
import MainLayout from '../components/layout/MainLayout';
import { FiMail, FiPhone, FiMapPin } 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';
import { facrApi } from '../services/facr/facrApi';
import { getCompetitionAliasesPublic } from '../services/competitionAliases';
type ContactFormData = {
name: string;
@@ -56,6 +57,9 @@ const ContactPage: React.FC = () => {
// Public contacts (grouped by category)
const [contactsData, setContactsData] = useState<GroupedContacts | null>(null);
const [contactsLoading, setContactsLoading] = useState(true);
// Club competitions (for tabs fallback) and aliases map
const [competitions, setCompetitions] = useState<Array<{ code?: string; name: string }>>([]);
const [aliasesMap, setAliasesMap] = useState<Record<string, string>>({});
const {
register,
@@ -121,6 +125,32 @@ const ContactPage: React.FC = () => {
return () => { mounted = false; };
}, []);
// Load club competitions + aliases (used to populate tabs when no contact categories are defined)
useEffect(() => {
(async () => {
try {
const clubId = (settings as any)?.club_id || '';
const clubType = ((settings as any)?.club_type || 'football') as 'football' | 'futsal';
let comps: Array<{ code?: string; name: string }> = [];
if (clubId) {
try {
const club = await facrApi.getClub(String(clubId), clubType);
const arr = Array.isArray((club as any)?.competitions) ? (club as any).competitions : [];
arr.forEach((c: any) => comps.push({ code: c.code, name: c.name || c.code }));
} catch {}
}
let amap: Record<string, string> = {};
try {
const list = await getCompetitionAliasesPublic();
list.forEach((a) => { if (a.code && a.alias) amap[a.code] = a.alias; });
} catch {}
const withAliases = comps.map((c) => ({ code: c.code, name: (c.code && amap[c.code]) ? amap[c.code] : c.name }));
setAliasesMap(amap);
setCompetitions(withAliases);
} catch {}
})();
}, [settings]);
const onSubmit = (data: ContactFormData) => {
mutate(data);
};
@@ -222,91 +252,148 @@ const ContactPage: React.FC = () => {
{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>
<Tabs colorScheme="blue" isFitted isLazy>
{(() => {
const categoryEntries = Object.entries(contactsData?.categories || {});
const compNames = competitions.map((c) => (c.code && aliasesMap[c.code]) ? aliasesMap[c.code] : c.name).filter(Boolean);
const useCategories = categoryEntries.length > 0;
const tabs = useCategories ? categoryEntries.map(([n]) => n) : compNames;
const hasOthers = uncategorized.length > 0;
return (
<>
<TabList>
{tabs.map((n) => (
<Tab key={n}>{n}</Tab>
))}
</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>
{hasOthers && <Tab>Ostatní</Tab>}
</TabList>
<TabPanels>
{useCategories
? categoryEntries.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>
))
: tabs.map((name) => {
const persons = (contactsData?.categories || {})[name] || [];
return (
<TabPanel key={name} pt={4}>
{persons.length > 0 ? (
<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>
) : (
<Text color="gray.500">Pro tuto kategorii zatím nemáme kontaktní osobu.</Text>
)}
</TabPanel>
);
})}
{hasOthers && (
<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>
)}
@@ -422,9 +509,6 @@ const ContactPage: React.FC = () => {
</Box>
</VStack>
</Container>
{/* Sponsors Section */}
<SponsorsSection />
</MainLayout>
);
};