This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user