main
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 3m37s

This commit is contained in:
2026-05-05 01:04:50 +03:00
parent f72857a3b2
commit e3cc6702dd
6 changed files with 571 additions and 212 deletions
+222 -25
View File
@@ -12,17 +12,50 @@ import {
Spinner,
Button,
Card,
Grid,
SimpleGrid,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp } from "@/components/motion";
import { SlideUp, FadeIn } from "@/components/motion";
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
import { usePrediction } from "@/lib/api/predictions/use-hooks";
import PredictionCard from "@/components/matches/prediction-card";
import OddsCard from "@/components/matches/odds-card";
import LineupsCard from "@/components/matches/lineups-card";
import { LuArrowLeft, LuRefreshCw } from "react-icons/lu";
import { LuArrowLeft, LuRefreshCw, LuShield, LuFlag, LuUser } from "react-icons/lu";
import type { MatchResponseDto } from "@/lib/api/matches/types";
// ─────────────────────────────────────────────────
// Type helpers for sidelined data
// ─────────────────────────────────────────────────
interface SidelinedPlayer {
playerName: string;
position?: string;
positionShort?: string;
description?: string;
type?: string;
matchesMissed?: number;
reasonIcon?: string;
}
interface SidelinedTeam {
teamId?: string;
teamName?: string;
totalSidelined?: number;
players?: SidelinedPlayer[];
}
interface SidelinedData {
homeTeam?: SidelinedTeam;
awayTeam?: SidelinedTeam;
}
// ─────────────────────────────────────────────────
// Main Component
// ─────────────────────────────────────────────────
export default function MatchDetailContent() {
const t = useTranslations("matches");
@@ -41,9 +74,13 @@ export default function MatchDetailContent() {
} = usePrediction(matchId);
const headerBg = useColorModeValue("white", "gray.800");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const subtleBg = useColorModeValue("gray.50", "gray.900");
const injuryBg = useColorModeValue("red.50", "rgba(254, 178, 178, 0.08)");
const injuryBorder = useColorModeValue("red.100", "red.800");
const match = matchData?.data;
const match = matchData?.data as MatchResponseDto | undefined;
const prediction = predictionData?.data;
if (matchLoading) {
@@ -70,6 +107,10 @@ export default function MatchDetailContent() {
const isLive = match.status === "LIVE";
const isFinished = match.status === "Finished";
const sidelined = (match.sidelined || {}) as SidelinedData;
const hasSidelined =
(sidelined.homeTeam?.players?.length || 0) > 0 ||
(sidelined.awayTeam?.players?.length || 0) > 0;
return (
<SlideUp>
@@ -85,26 +126,32 @@ export default function MatchDetailContent() {
<LuArrowLeft />
{tCommon("back")}
</Button>
{/* Match Header */}
{/* ═══════════════════════════════════════════ */}
{/* Match Header Card */}
{/* ═══════════════════════════════════════════ */}
<Card.Root
bg={headerBg}
borderColor={borderColor}
borderRadius="xl"
mb={6}
overflow="hidden"
>
<Card.Body>
{/* League Info */}
{match.league && (
<Flex justify="center" align="center" gap={2} mb={4}>
{/* League Banner */}
{match.league && (
<Box bg={subtleBg} px={4} py={2.5} borderBottomWidth="1px" borderColor={borderColor}>
<Flex justify="center" align="center" gap={2}>
{match.league.country?.flag && (
<Image
src={match.league.country.flag}
alt={match.league.country.name || ""}
boxSize="18px"
objectFit="contain"
borderRadius="sm"
/>
)}
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
{match.league.country?.name && `${match.league.country.name}`}
{match.league.name}
</Text>
<Badge
@@ -132,8 +179,10 @@ export default function MatchDetailContent() {
: t("not-started")}
</Badge>
</Flex>
)}
</Box>
)}
<Card.Body py={6}>
{/* Teams & Score */}
<HStack gap={6} justify="center" align="center">
{/* Home Team */}
@@ -142,12 +191,12 @@ export default function MatchDetailContent() {
<Image
src={match.homeTeam.logo}
alt={match.homeTeam.name}
boxSize="64px"
boxSize="72px"
objectFit="contain"
/>
) : (
<Flex
boxSize="64px"
boxSize="72px"
bg="primary.subtle"
borderRadius="full"
align="center"
@@ -161,9 +210,9 @@ export default function MatchDetailContent() {
<Text fontSize="md" fontWeight="bold" textAlign="center">
{match.homeTeam?.name}
</Text>
<Text fontSize="xs" color="fg.muted">
<Badge variant="subtle" colorPalette="blue" fontSize="2xs">
{t("home-team")}
</Text>
</Badge>
</VStack>
{/* Score */}
@@ -171,18 +220,20 @@ export default function MatchDetailContent() {
{match.score && (isLive || isFinished) ? (
<HStack gap={3}>
<Text
fontSize="4xl"
fontSize="5xl"
fontWeight="900"
lineHeight="1"
color={isLive ? "red.500" : "fg"}
>
{match.score.home}
</Text>
<Text fontSize="2xl" color="fg.muted">
-
<Text fontSize="2xl" color="fg.muted" fontWeight="300">
:
</Text>
<Text
fontSize="4xl"
fontSize="5xl"
fontWeight="900"
lineHeight="1"
color={isLive ? "red.500" : "fg"}
>
{match.score.away}
@@ -211,12 +262,12 @@ export default function MatchDetailContent() {
<Image
src={match.awayTeam.logo}
alt={match.awayTeam.name}
boxSize="64px"
boxSize="72px"
objectFit="contain"
/>
) : (
<Flex
boxSize="64px"
boxSize="72px"
bg="primary.subtle"
borderRadius="full"
align="center"
@@ -230,18 +281,77 @@ export default function MatchDetailContent() {
<Text fontSize="md" fontWeight="bold" textAlign="center">
{match.awayTeam?.name}
</Text>
<Text fontSize="xs" color="fg.muted">
<Badge variant="subtle" colorPalette="orange" fontSize="2xs">
{t("away-team")}
</Text>
</Badge>
</VStack>
</HStack>
{/* Referee Info */}
{match.refereeName && (
<Flex
justify="center"
align="center"
gap={1.5}
mt={4}
pt={3}
borderTopWidth="1px"
borderColor={borderColor}
>
<LuUser size={14} />
<Text fontSize="xs" color="fg.muted">
{t("referee")}: <Text as="span" fontWeight="semibold" color="fg">{match.refereeName}</Text>
</Text>
</Flex>
)}
</Card.Body>
</Card.Root>
{/* Lineups Section */}
{/* ═══════════════════════════════════════════ */}
{/* Sidelined / Injuries Card */}
{/* ═══════════════════════════════════════════ */}
{hasSidelined && (
<FadeIn>
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
<Card.Body>
<Heading as="h2" size="md" mb={4}>
🏥 {t("sidelined")}
</Heading>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
{/* Home Team Sidelined */}
<SidelinedColumn
team={sidelined.homeTeam}
teamName={match.homeTeam?.name || ""}
teamLogo={match.homeTeam?.logo}
injuryBg={injuryBg}
injuryBorder={injuryBorder}
t={t}
/>
{/* Away Team Sidelined */}
<SidelinedColumn
team={sidelined.awayTeam}
teamName={match.awayTeam?.name || ""}
teamLogo={match.awayTeam?.logo}
injuryBg={injuryBg}
injuryBorder={injuryBorder}
t={t}
/>
</SimpleGrid>
</Card.Body>
</Card.Root>
</FadeIn>
)}
{/* ═══════════════════════════════════════════ */}
{/* Lineups Section */}
{/* ═══════════════════════════════════════════ */}
<LineupsCard match={match} prediction={prediction} />
{/* Prediction Section */}
{/* ═══════════════════════════════════════════ */}
{/* Prediction Section */}
{/* ═══════════════════════════════════════════ */}
<Box>
<Flex justify="space-between" align="center" mb={4}>
<Heading as="h2" size="lg">
@@ -275,7 +385,9 @@ export default function MatchDetailContent() {
)}
</Box>
{/* Odds Section */}
{/* ═══════════════════════════════════════════ */}
{/* Odds Section */}
{/* ═══════════════════════════════════════════ */}
{match.odds && Object.keys(match.odds).length > 0 && (
<OddsCard odds={match.odds} />
)}
@@ -283,3 +395,88 @@ export default function MatchDetailContent() {
</SlideUp>
);
}
// ─────────────────────────────────────────────────
// Sidelined Column Sub-Component
// ─────────────────────────────────────────────────
interface SidelinedColumnProps {
team?: SidelinedTeam;
teamName: string;
teamLogo?: string;
injuryBg: string;
injuryBorder: string;
t: ReturnType<typeof useTranslations>;
}
function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }: SidelinedColumnProps) {
const players = team?.players || [];
if (players.length === 0) {
return (
<Box>
<HStack gap={2} mb={3}>
{teamLogo && <Image src={teamLogo} alt={teamName} boxSize="20px" objectFit="contain" />}
<Text fontSize="sm" fontWeight="bold">{teamName}</Text>
</HStack>
<Text fontSize="xs" color="fg.muted" fontStyle="italic">
{t("no-sidelined")}
</Text>
</Box>
);
}
return (
<Box>
<HStack gap={2} mb={3}>
{teamLogo && <Image src={teamLogo} alt={teamName} boxSize="20px" objectFit="contain" />}
<Text fontSize="sm" fontWeight="bold">{teamName}</Text>
<Badge colorPalette="red" variant="subtle" fontSize="2xs" borderRadius="full">
{players.length}
</Badge>
</HStack>
<VStack gap={2} align="stretch">
{players.map((player, idx) => (
<Box
key={`${player.playerName}-${idx}`}
bg={injuryBg}
borderWidth="1px"
borderColor={injuryBorder}
borderRadius="lg"
px={3}
py={2}
>
<Flex justify="space-between" align="center">
<VStack gap={0} align="start">
<Text fontSize="sm" fontWeight="semibold">
{player.playerName}
</Text>
<HStack gap={1.5}>
{player.positionShort && (
<Badge variant="outline" fontSize="2xs" borderRadius="full">
{player.positionShort}
</Badge>
)}
<Text fontSize="xs" color="fg.muted">
{player.description || (
player.type === "injury"
? t("injury")
: player.type === "suspended"
? t("suspended")
: t("other-reason")
)}
</Text>
</HStack>
</VStack>
{player.matchesMissed !== undefined && player.matchesMissed > 0 && (
<Badge colorPalette="red" variant="subtle" fontSize="2xs">
{player.matchesMissed} {t("matches-missed")}
</Badge>
)}
</Flex>
</Box>
))}
</VStack>
</Box>
);
}