first
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 4m0s

This commit is contained in:
2026-04-16 13:36:34 +03:00
parent de5e145c4e
commit fc7a1ba567
218 changed files with 32370 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
"use client";
import {
Box,
Flex,
Heading,
Text,
Card,
VStack,
HStack,
Badge,
Spinner,
Input,
Button,
} from "@chakra-ui/react";
import { InputGroup } from "@/components/ui/forms/input-group";
import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp } from "@/components/motion";
import { useSearchTeams, useHeadToHead } from "@/lib/api/leagues/use-hooks";
import type { TeamDto, HeadToHeadDto } from "@/lib/api/leagues/types";
import type { MatchResponseDto } from "@/lib/api/matches/types";
import { LuSearch, LuArrowLeftRight } from "react-icons/lu";
import { useState, useEffect } from "react";
import { useDebounce } from "@/hooks/use-debounce";
function TeamSearchInput({
label,
value,
onSelect,
}: {
label: string;
value: TeamDto | null;
onSelect: (team: TeamDto) => void;
}) {
const t = useTranslations("h2h");
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const searchTeams = useSearchTeams(
debouncedQuery.length >= 2 ? { q: debouncedQuery } : { q: "" },
);
return (
<Box position="relative" w="full">
<Text fontWeight="semibold" mb={2}>
{label}
</Text>
<InputGroup startElement={<LuSearch />}>
<Input
value={value ? value.name : query}
onChange={(e) => {
setQuery(e.target.value);
if (value) onSelect(null as unknown as TeamDto);
}}
placeholder={t("search-team")}
/>
</InputGroup>
{debouncedQuery.length >= 2 && !value && searchTeams.data?.data && (
<Box
position="absolute"
top="full"
left={0}
right={0}
bg="bg.panel"
border="1px"
borderColor="border.muted"
borderRadius="md"
zIndex={10}
maxH="200px"
overflowY="auto"
>
{searchTeams.data.data.map((team: TeamDto) => (
<Flex
key={team.id}
px={3}
py={2}
cursor="pointer"
_hover={{ bg: "gray.100", _dark: { bg: "gray.700" } }}
onClick={() => onSelect(team)}
align="center"
gap={2}
>
{team.logo ? (
<img
src={team.logo}
width="20"
height="20"
style={{ borderRadius: "50%" }}
alt={team.name}
/>
) : null}
<Text fontSize="sm">{team.name}</Text>
{team.sport ? (
<Badge
size="xs"
colorScheme={team.sport === "football" ? "green" : "orange"}
>
{team.sport}
</Badge>
) : null}
</Flex>
))}
</Box>
)}
</Box>
);
}
export default function H2HContent() {
const t = useTranslations("h2h");
const tMatches = useTranslations("matches");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const [team1, setTeam1] = useState<TeamDto | null>(null);
const [team2, setTeam2] = useState<TeamDto | null>(null);
const [hasSearched, setHasSearched] = useState(false);
const h2h = useHeadToHead(
team1 && team2
? { team1: team1.id, team2: team2.id }
: { team1: "", team2: "" },
);
const handleSearch = () => {
if (team1 && team2) {
setHasSearched(true);
h2h.refetch();
}
};
const stats: { label: string; value: number; color: string }[] = h2h.data
?.data
? [
{
label: team1?.name || t("team1"),
value: h2h.data.data.team1Wins,
color: "green",
},
{
label: t("draws"),
value: h2h.data.data.draws,
color: "gray",
},
{
label: team2?.name || t("team2"),
value: h2h.data.data.team2Wins,
color: "blue",
},
]
: [];
return (
<SlideUp>
<Box maxW="5xl" mx="auto">
<Heading as="h1" size="xl" fontWeight="bold" mb={6}>
<HStack gap={2}>
<LuArrowLeftRight />
<Text>{t("title")}</Text>
</HStack>
</Heading>
{/* Team Selection */}
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
mb={6}
>
<Card.Body>
<Flex
direction={{ base: "column", md: "row" }}
gap={4}
align="flex-end"
>
<Box flex={1}>
<TeamSearchInput
label={t("team-1")}
value={team1}
onSelect={(t) => setTeam1(t)}
/>
</Box>
<Box flex={1}>
<TeamSearchInput
label={t("team-2")}
value={team2}
onSelect={(t) => setTeam2(t)}
/>
</Box>
<Button
onClick={handleSearch}
disabled={!team1 || !team2}
minW="120px"
>
{t("compare")}
</Button>
</Flex>
</Card.Body>
</Card.Root>
{/* Results */}
{hasSearched && (
<>
{/* Stats Bar */}
{h2h.isLoading ? (
<Flex justify="center" py={8}>
<Spinner size="md" color="primary.500" />
</Flex>
) : h2h.data?.data ? (
<>
<Flex gap={4} mb={6} justify="center">
{stats.map((s) => (
<Card.Root
key={s.label}
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
flex={1}
maxW="200px"
>
<Card.Body textAlign="center">
<Text
fontSize="3xl"
fontWeight="bold"
color={`${s.color}.500`}
>
{s.value}
</Text>
<Text fontSize="sm" color="fg.muted" mt={1}>
{s.label}
</Text>
</Card.Body>
</Card.Root>
))}
</Flex>
{/* Match History */}
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
>
<Card.Header>
<Heading as="h3" size="sm">
{tMatches("recent-matches")} (
{h2h.data.data.matches?.length ?? 0})
</Heading>
</Card.Header>
<Card.Body pt={0}>
<VStack gap={3}>
{(
h2h.data.data.matches as
| MatchResponseDto[]
| undefined
| null
)?.map((match: MatchResponseDto) => {
const isHomeTeam1 = match.homeTeam?.id === team1?.id;
// Backend returns scoreHome/scoreAway, not homeScore/awayScore
const homeScore = Number((match as any).scoreHome ?? 0);
const awayScore = Number((match as any).scoreAway ?? 0);
const homeWon =
(isHomeTeam1 && homeScore > awayScore) ||
(!isHomeTeam1 && awayScore > homeScore);
const isDraw = homeScore === awayScore;
// Parse mstUtc - can be bigint string from backend
const matchDate = match.mstUtc
? new Date(Number(match.mstUtc)).toLocaleDateString()
: "";
return (
<Flex
key={match.id}
p={3}
borderRadius="md"
bg={
isDraw
? "gray.50"
: homeWon
? "green.50"
: "red.50"
}
_dark={{
bg: isDraw
? "gray.750"
: homeWon
? "green.900"
: "red.900",
}}
justify="space-between"
align="center"
>
<Flex align="center" gap={3} flex={1}>
<Text fontSize="sm" fontWeight="medium">
{match.homeTeam?.name}
</Text>
<Badge
colorScheme={
isDraw ? "gray" : homeWon ? "green" : "red"
}
>
{homeScore ?? 0} - {awayScore ?? 0}
</Badge>
<Text fontSize="sm" fontWeight="medium">
{match.awayTeam?.name}
</Text>
</Flex>
<Text fontSize="xs" color="fg.muted">
{matchDate}
</Text>
</Flex>
);
})}
</VStack>
</Card.Body>
</Card.Root>
</>
) : (
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
>
<Card.Body textAlign="center" py={8}>
<Text color="fg.muted">{t("no-matches-found")}</Text>
</Card.Body>
</Card.Root>
)}
</>
)}
</Box>
</SlideUp>
);
}