This commit is contained in:
+91
-2
@@ -36,7 +36,9 @@
|
||||
"logging-in": "Signing in...",
|
||||
"registering": "Creating account...",
|
||||
"login-success": "Login successful!",
|
||||
"register-success": "Registration successful!"
|
||||
"register-success": "Registration successful!",
|
||||
"login-required-title": "Login Required",
|
||||
"login-required-message": "Please sign in or create an account to view match analysis."
|
||||
},
|
||||
"all-right-reserved": "All rights reserved.",
|
||||
"privacy-policy": "Privacy Policy",
|
||||
@@ -121,7 +123,22 @@
|
||||
"recent-matches": "Recent Matches",
|
||||
"home-team": "Home",
|
||||
"away-team": "Away",
|
||||
"vs": "vs"
|
||||
"vs": "vs",
|
||||
"referee": "Referee",
|
||||
"sidelined": "Injuries & Absences",
|
||||
"injury": "Injury",
|
||||
"suspended": "Suspended",
|
||||
"other-reason": "Other",
|
||||
"matches-missed": "Matches Missed",
|
||||
"position": "Position",
|
||||
"no-sidelined": "No injury information available",
|
||||
"match-events": "Match Events",
|
||||
"goal": "Goal",
|
||||
"yellow-card": "Yellow Card",
|
||||
"red-card": "Red Card",
|
||||
"substitution": "Substitution",
|
||||
"starters": "Starting XI",
|
||||
"substitutes": "Substitutes"
|
||||
},
|
||||
|
||||
"predictions": {
|
||||
@@ -517,5 +534,77 @@
|
||||
"items-per-page": "Items per page",
|
||||
"showing": "Showing",
|
||||
"results": "results"
|
||||
},
|
||||
|
||||
"seo": {
|
||||
"global": {
|
||||
"title": "iddaai.com | AI-Powered Betting Predictions",
|
||||
"description": "iddaai.com offers AI-powered betting predictions, detailed match analysis, and data-driven coupon building.",
|
||||
"keywords": "betting, betting predictions, AI betting, match analysis, sure bets, football statistics"
|
||||
},
|
||||
"home": {
|
||||
"title": "Home",
|
||||
"description": "AI-powered betting predictions. Analyze matches, discover value bets, and build winning coupons."
|
||||
},
|
||||
"h2h": {
|
||||
"title": "Head to Head Comparison",
|
||||
"description": "Compare football teams head to head. In-depth statistics, past matches, and predictions."
|
||||
},
|
||||
"analysis": {
|
||||
"title": "Multi-Match Analysis",
|
||||
"description": "Analyze multiple matches at once. Detailed statistics and AI-driven strategies."
|
||||
},
|
||||
"leagues": {
|
||||
"title": "Leagues & Teams",
|
||||
"description": "Explore football and basketball leagues, countries, and team statistics worldwide."
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin Panel",
|
||||
"description": "Admin panel for managing users and system settings."
|
||||
},
|
||||
"matches": {
|
||||
"title": "Matches & Fixtures",
|
||||
"description": "View upcoming matches, live scores, and past fixtures with AI predictions."
|
||||
},
|
||||
"about": {
|
||||
"title": "About Us",
|
||||
"description": "Learn more about iddaai.com, our AI technology, and how we deliver betting insights."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Your personalized dashboard for betting stats, predictions, and account overview."
|
||||
},
|
||||
"profile": {
|
||||
"title": "My Profile",
|
||||
"description": "Manage your user profile, subscription, and account settings."
|
||||
},
|
||||
"spor-toto": {
|
||||
"title": "Spor Toto Predictions",
|
||||
"description": "AI-powered Spor Toto predictions. Build coupons with conservative, balanced, or aggressive strategies."
|
||||
},
|
||||
"coupon-builder": {
|
||||
"title": "AI Coupon Builder",
|
||||
"description": "Automatically generate optimized betting coupons using advanced AI and statistical models."
|
||||
},
|
||||
"teams": {
|
||||
"title": "Team Statistics",
|
||||
"description": "Detailed statistics, form analysis, and predictive models for football teams."
|
||||
},
|
||||
"coupon-history": {
|
||||
"title": "Coupon History",
|
||||
"description": "Review your past betting coupons and track your performance."
|
||||
},
|
||||
"predictions": {
|
||||
"title": "Betting Predictions",
|
||||
"description": "Daily AI betting predictions, value odds, and high-confidence match tips."
|
||||
},
|
||||
"signup": {
|
||||
"title": "Sign Up",
|
||||
"description": "Create your iddaai.com account to access AI-powered betting predictions."
|
||||
},
|
||||
"signin": {
|
||||
"title": "Sign In",
|
||||
"description": "Sign in to your iddaai.com account to access AI predictions and tools."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -36,7 +36,9 @@
|
||||
"logging-in": "Giriş yapılıyor...",
|
||||
"registering": "Hesap oluşturuluyor...",
|
||||
"login-success": "Giriş başarılı!",
|
||||
"register-success": "Kayıt başarılı!"
|
||||
"register-success": "Kayıt başarılı!",
|
||||
"login-required-title": "Giriş Yapmanız Gerekiyor",
|
||||
"login-required-message": "Maç analizlerini görüntülemek için lütfen giriş yapın veya hesap oluşturun."
|
||||
},
|
||||
"all-right-reserved": "Tüm hakları saklıdır.",
|
||||
"privacy-policy": "Gizlilik Politikası",
|
||||
@@ -117,7 +119,22 @@
|
||||
"recent-matches": "Son Maçlar",
|
||||
"home-team": "Ev Sahibi",
|
||||
"away-team": "Deplasman",
|
||||
"vs": "vs"
|
||||
"vs": "vs",
|
||||
"referee": "Hakem",
|
||||
"sidelined": "Sakatlık & Eksikler",
|
||||
"injury": "Sakatlık",
|
||||
"suspended": "Cezalı",
|
||||
"other-reason": "Diğer",
|
||||
"matches-missed": "Kaçırılan Maç",
|
||||
"position": "Pozisyon",
|
||||
"no-sidelined": "Eksik oyuncu bilgisi yok",
|
||||
"match-events": "Maç Olayları",
|
||||
"goal": "Gol",
|
||||
"yellow-card": "Sarı Kart",
|
||||
"red-card": "Kırmızı Kart",
|
||||
"substitution": "Oyuncu Değişikliği",
|
||||
"starters": "İlk 11",
|
||||
"substitutes": "Yedekler"
|
||||
},
|
||||
"predictions": {
|
||||
"title": "Tahminler",
|
||||
|
||||
@@ -11,10 +11,13 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { slideUpVariants } from "@/components/motion";
|
||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { useState } from "react";
|
||||
import { LoginModal } from "@/components/auth/login-modal";
|
||||
|
||||
interface MatchCardProps {
|
||||
match: MatchResponseDto;
|
||||
@@ -24,7 +27,10 @@ const MotionBox = motion.create(Box);
|
||||
|
||||
export default function MatchCard({ match }: MatchCardProps) {
|
||||
const t = useTranslations("matches");
|
||||
const tAuth = useTranslations("auth");
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const cardBorder = useColorModeValue("gray.100", "gray.700");
|
||||
@@ -42,6 +48,10 @@ export default function MatchCard({ match }: MatchCardProps) {
|
||||
: t("not-started");
|
||||
|
||||
const handleClick = () => {
|
||||
if (!session) {
|
||||
setLoginModalOpen(true);
|
||||
return;
|
||||
}
|
||||
router.push(`/matches/${match.id}`);
|
||||
};
|
||||
|
||||
@@ -49,6 +59,7 @@ export default function MatchCard({ match }: MatchCardProps) {
|
||||
const matchDate = new Date(match.mstUtc);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MotionBox
|
||||
variants={slideUpVariants}
|
||||
bg={cardBg}
|
||||
@@ -223,6 +234,30 @@ export default function MatchCard({ match }: MatchCardProps) {
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* Auth hint for unauthenticated users */}
|
||||
{!session && (
|
||||
<Flex
|
||||
mt={2}
|
||||
pt={2}
|
||||
borderTopWidth="1px"
|
||||
borderColor={cardBorder}
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<Text fontSize="xs" color="orange.500" fontWeight="semibold">
|
||||
🔒 {tAuth("login-required-title")}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</MotionBox>
|
||||
|
||||
{/* Login Modal — shown when unauthenticated user clicks a match */}
|
||||
<LoginModal
|
||||
open={loginModalOpen}
|
||||
onOpenChange={setLoginModalOpen}
|
||||
initialMode="login"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
{/* League Banner */}
|
||||
{match.league && (
|
||||
<Flex justify="center" align="center" gap={2} mb={4}>
|
||||
<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>
|
||||
|
||||
{/* ═══════════════════════════════════════════ */}
|
||||
{/* 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 */}
|
||||
{/* ═══════════════════════════════════════════ */}
|
||||
<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 */}
|
||||
{/* ═══════════════════════════════════════════ */}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { SlideUp, FadeIn } from "@/components/motion";
|
||||
import { useTeamById, useTeamMatches } from "@/lib/api/leagues/use-hooks";
|
||||
import { LuArrowLeft, LuCalendar, LuTrophy, LuChevronDown } from "react-icons/lu";
|
||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { LoginModal } from "@/components/auth/login-modal";
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// Utility Functions
|
||||
@@ -32,7 +34,7 @@ function getMatchTimestamp(match: MatchResponseDto): number {
|
||||
}
|
||||
|
||||
function getMatchStatus(match: MatchResponseDto): string {
|
||||
return String(match.status || (match as Record<string, unknown>).state || "").toUpperCase();
|
||||
return String(match.status || match.state || "").toUpperCase();
|
||||
}
|
||||
|
||||
function isMatchFinished(match: MatchResponseDto): boolean {
|
||||
@@ -50,7 +52,7 @@ function getTeamSideName(team: MatchResponseDto["homeTeam"] | MatchResponseDto["
|
||||
}
|
||||
|
||||
function getTeamSideLogo(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
|
||||
return String(team?.logo || (team as Record<string, unknown> | undefined)?.logoUrl || fallback || "");
|
||||
return String(team?.logo || fallback || "");
|
||||
}
|
||||
|
||||
function getLeagueLabel(match: MatchResponseDto): string {
|
||||
@@ -83,6 +85,8 @@ export default function TeamDetailContent() {
|
||||
const t = useTranslations();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
|
||||
const teamId = params.id as string;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -245,7 +249,7 @@ export default function TeamDetailContent() {
|
||||
cardBg={cardBg}
|
||||
borderColor={borderColor}
|
||||
statusBadge={getStatusBadge(match)}
|
||||
onClick={() => router.push(`/tr/matches/${match.id}`)}
|
||||
onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
@@ -316,7 +320,7 @@ export default function TeamDetailContent() {
|
||||
cardBg={cardBg}
|
||||
borderColor={borderColor}
|
||||
statusBadge={getStatusBadge(match)}
|
||||
onClick={() => router.push(`/tr/matches/${match.id}`)}
|
||||
onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
@@ -379,6 +383,13 @@ export default function TeamDetailContent() {
|
||||
)}
|
||||
</Box>
|
||||
</FadeIn>
|
||||
|
||||
{/* Login Modal — shown when unauthenticated user clicks a match */}
|
||||
<LoginModal
|
||||
open={loginModalOpen}
|
||||
onOpenChange={setLoginModalOpen}
|
||||
initialMode="login"
|
||||
/>
|
||||
</Box>
|
||||
</SlideUp>
|
||||
);
|
||||
|
||||
@@ -83,31 +83,41 @@ export interface MatchResponseDto {
|
||||
odds?: Record<string, Record<string, { odd: string }>>;
|
||||
|
||||
// Nested Objects (from Backend include)
|
||||
homeTeam?: { name: string; logo?: string; [key: string]: unknown };
|
||||
awayTeam?: { name: string; logo?: string; [key: string]: unknown };
|
||||
homeTeam?: { id?: string; name: string; logo?: string };
|
||||
awayTeam?: { id?: string; name: string; logo?: string };
|
||||
league?: {
|
||||
name: string;
|
||||
country?: { name: string; flag?: string };
|
||||
[key: string]: unknown;
|
||||
};
|
||||
lineups?: {
|
||||
home: Array<{
|
||||
player?: { name: string; id: string; [key: string]: unknown };
|
||||
player?: { name: string; id: string };
|
||||
position?: string | null;
|
||||
shirtNumber?: number | null;
|
||||
isStarting?: boolean;
|
||||
[key: string]: unknown;
|
||||
isProbable?: boolean;
|
||||
lineupSource?: string;
|
||||
projectionConfidence?: number;
|
||||
}>;
|
||||
away: Array<{
|
||||
player?: { name: string; id: string; [key: string]: unknown };
|
||||
player?: { name: string; id: string };
|
||||
position?: string | null;
|
||||
shirtNumber?: number | null;
|
||||
isStarting?: boolean;
|
||||
[key: string]: unknown;
|
||||
isProbable?: boolean;
|
||||
lineupSource?: string;
|
||||
projectionConfidence?: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
[key: string]: unknown;
|
||||
// Match detail enrichments
|
||||
refereeName?: string;
|
||||
sidelined?: Record<string, unknown>;
|
||||
events?: Array<Record<string, unknown>>;
|
||||
|
||||
// Additional fields from backend detail endpoint
|
||||
lineupSource?: string;
|
||||
stats?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ActiveLeagueDto {
|
||||
|
||||
Reference in New Issue
Block a user