This commit is contained in:
+91
-2
@@ -36,7 +36,9 @@
|
|||||||
"logging-in": "Signing in...",
|
"logging-in": "Signing in...",
|
||||||
"registering": "Creating account...",
|
"registering": "Creating account...",
|
||||||
"login-success": "Login successful!",
|
"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.",
|
"all-right-reserved": "All rights reserved.",
|
||||||
"privacy-policy": "Privacy Policy",
|
"privacy-policy": "Privacy Policy",
|
||||||
@@ -121,7 +123,22 @@
|
|||||||
"recent-matches": "Recent Matches",
|
"recent-matches": "Recent Matches",
|
||||||
"home-team": "Home",
|
"home-team": "Home",
|
||||||
"away-team": "Away",
|
"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": {
|
"predictions": {
|
||||||
@@ -517,5 +534,77 @@
|
|||||||
"items-per-page": "Items per page",
|
"items-per-page": "Items per page",
|
||||||
"showing": "Showing",
|
"showing": "Showing",
|
||||||
"results": "results"
|
"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...",
|
"logging-in": "Giriş yapılıyor...",
|
||||||
"registering": "Hesap oluşturuluyor...",
|
"registering": "Hesap oluşturuluyor...",
|
||||||
"login-success": "Giriş başarılı!",
|
"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.",
|
"all-right-reserved": "Tüm hakları saklıdır.",
|
||||||
"privacy-policy": "Gizlilik Politikası",
|
"privacy-policy": "Gizlilik Politikası",
|
||||||
@@ -117,7 +119,22 @@
|
|||||||
"recent-matches": "Son Maçlar",
|
"recent-matches": "Son Maçlar",
|
||||||
"home-team": "Ev Sahibi",
|
"home-team": "Ev Sahibi",
|
||||||
"away-team": "Deplasman",
|
"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": {
|
"predictions": {
|
||||||
"title": "Tahminler",
|
"title": "Tahminler",
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { slideUpVariants } from "@/components/motion";
|
import { slideUpVariants } from "@/components/motion";
|
||||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { LoginModal } from "@/components/auth/login-modal";
|
||||||
|
|
||||||
interface MatchCardProps {
|
interface MatchCardProps {
|
||||||
match: MatchResponseDto;
|
match: MatchResponseDto;
|
||||||
@@ -24,7 +27,10 @@ const MotionBox = motion.create(Box);
|
|||||||
|
|
||||||
export default function MatchCard({ match }: MatchCardProps) {
|
export default function MatchCard({ match }: MatchCardProps) {
|
||||||
const t = useTranslations("matches");
|
const t = useTranslations("matches");
|
||||||
|
const tAuth = useTranslations("auth");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const cardBorder = useColorModeValue("gray.100", "gray.700");
|
const cardBorder = useColorModeValue("gray.100", "gray.700");
|
||||||
@@ -42,6 +48,10 @@ export default function MatchCard({ match }: MatchCardProps) {
|
|||||||
: t("not-started");
|
: t("not-started");
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
|
if (!session) {
|
||||||
|
setLoginModalOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
router.push(`/matches/${match.id}`);
|
router.push(`/matches/${match.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +59,7 @@ export default function MatchCard({ match }: MatchCardProps) {
|
|||||||
const matchDate = new Date(match.mstUtc);
|
const matchDate = new Date(match.mstUtc);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<MotionBox
|
<MotionBox
|
||||||
variants={slideUpVariants}
|
variants={slideUpVariants}
|
||||||
bg={cardBg}
|
bg={cardBg}
|
||||||
@@ -223,6 +234,30 @@ export default function MatchCard({ match }: MatchCardProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</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>
|
</MotionBox>
|
||||||
|
|
||||||
|
{/* Login Modal — shown when unauthenticated user clicks a match */}
|
||||||
|
<LoginModal
|
||||||
|
open={loginModalOpen}
|
||||||
|
onOpenChange={setLoginModalOpen}
|
||||||
|
initialMode="login"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,50 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Grid,
|
||||||
|
SimpleGrid,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
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 { useMatchDetails } from "@/lib/api/matches/use-hooks";
|
||||||
import { usePrediction } from "@/lib/api/predictions/use-hooks";
|
import { usePrediction } from "@/lib/api/predictions/use-hooks";
|
||||||
import PredictionCard from "@/components/matches/prediction-card";
|
import PredictionCard from "@/components/matches/prediction-card";
|
||||||
import OddsCard from "@/components/matches/odds-card";
|
import OddsCard from "@/components/matches/odds-card";
|
||||||
import LineupsCard from "@/components/matches/lineups-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() {
|
export default function MatchDetailContent() {
|
||||||
const t = useTranslations("matches");
|
const t = useTranslations("matches");
|
||||||
@@ -41,9 +74,13 @@ export default function MatchDetailContent() {
|
|||||||
} = usePrediction(matchId);
|
} = usePrediction(matchId);
|
||||||
|
|
||||||
const headerBg = useColorModeValue("white", "gray.800");
|
const headerBg = useColorModeValue("white", "gray.800");
|
||||||
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
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;
|
const prediction = predictionData?.data;
|
||||||
|
|
||||||
if (matchLoading) {
|
if (matchLoading) {
|
||||||
@@ -70,6 +107,10 @@ export default function MatchDetailContent() {
|
|||||||
|
|
||||||
const isLive = match.status === "LIVE";
|
const isLive = match.status === "LIVE";
|
||||||
const isFinished = match.status === "Finished";
|
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 (
|
return (
|
||||||
<SlideUp>
|
<SlideUp>
|
||||||
@@ -85,26 +126,32 @@ export default function MatchDetailContent() {
|
|||||||
<LuArrowLeft />
|
<LuArrowLeft />
|
||||||
{tCommon("back")}
|
{tCommon("back")}
|
||||||
</Button>
|
</Button>
|
||||||
{/* Match Header */}
|
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
|
{/* Match Header Card */}
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
<Card.Root
|
<Card.Root
|
||||||
bg={headerBg}
|
bg={headerBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
mb={6}
|
mb={6}
|
||||||
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
<Card.Body>
|
{/* League Banner */}
|
||||||
{/* League Info */}
|
|
||||||
{match.league && (
|
{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 && (
|
{match.league.country?.flag && (
|
||||||
<Image
|
<Image
|
||||||
src={match.league.country.flag}
|
src={match.league.country.flag}
|
||||||
alt={match.league.country.name || ""}
|
alt={match.league.country.name || ""}
|
||||||
boxSize="18px"
|
boxSize="18px"
|
||||||
objectFit="contain"
|
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}
|
{match.league.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Badge
|
<Badge
|
||||||
@@ -132,8 +179,10 @@ export default function MatchDetailContent() {
|
|||||||
: t("not-started")}
|
: t("not-started")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Card.Body py={6}>
|
||||||
{/* Teams & Score */}
|
{/* Teams & Score */}
|
||||||
<HStack gap={6} justify="center" align="center">
|
<HStack gap={6} justify="center" align="center">
|
||||||
{/* Home Team */}
|
{/* Home Team */}
|
||||||
@@ -142,12 +191,12 @@ export default function MatchDetailContent() {
|
|||||||
<Image
|
<Image
|
||||||
src={match.homeTeam.logo}
|
src={match.homeTeam.logo}
|
||||||
alt={match.homeTeam.name}
|
alt={match.homeTeam.name}
|
||||||
boxSize="64px"
|
boxSize="72px"
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Flex
|
<Flex
|
||||||
boxSize="64px"
|
boxSize="72px"
|
||||||
bg="primary.subtle"
|
bg="primary.subtle"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
align="center"
|
align="center"
|
||||||
@@ -161,9 +210,9 @@ export default function MatchDetailContent() {
|
|||||||
<Text fontSize="md" fontWeight="bold" textAlign="center">
|
<Text fontSize="md" fontWeight="bold" textAlign="center">
|
||||||
{match.homeTeam?.name}
|
{match.homeTeam?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Badge variant="subtle" colorPalette="blue" fontSize="2xs">
|
||||||
{t("home-team")}
|
{t("home-team")}
|
||||||
</Text>
|
</Badge>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
{/* Score */}
|
{/* Score */}
|
||||||
@@ -171,18 +220,20 @@ export default function MatchDetailContent() {
|
|||||||
{match.score && (isLive || isFinished) ? (
|
{match.score && (isLive || isFinished) ? (
|
||||||
<HStack gap={3}>
|
<HStack gap={3}>
|
||||||
<Text
|
<Text
|
||||||
fontSize="4xl"
|
fontSize="5xl"
|
||||||
fontWeight="900"
|
fontWeight="900"
|
||||||
|
lineHeight="1"
|
||||||
color={isLive ? "red.500" : "fg"}
|
color={isLive ? "red.500" : "fg"}
|
||||||
>
|
>
|
||||||
{match.score.home}
|
{match.score.home}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xl" color="fg.muted">
|
<Text fontSize="2xl" color="fg.muted" fontWeight="300">
|
||||||
-
|
:
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fontSize="4xl"
|
fontSize="5xl"
|
||||||
fontWeight="900"
|
fontWeight="900"
|
||||||
|
lineHeight="1"
|
||||||
color={isLive ? "red.500" : "fg"}
|
color={isLive ? "red.500" : "fg"}
|
||||||
>
|
>
|
||||||
{match.score.away}
|
{match.score.away}
|
||||||
@@ -211,12 +262,12 @@ export default function MatchDetailContent() {
|
|||||||
<Image
|
<Image
|
||||||
src={match.awayTeam.logo}
|
src={match.awayTeam.logo}
|
||||||
alt={match.awayTeam.name}
|
alt={match.awayTeam.name}
|
||||||
boxSize="64px"
|
boxSize="72px"
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Flex
|
<Flex
|
||||||
boxSize="64px"
|
boxSize="72px"
|
||||||
bg="primary.subtle"
|
bg="primary.subtle"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
align="center"
|
align="center"
|
||||||
@@ -230,18 +281,77 @@ export default function MatchDetailContent() {
|
|||||||
<Text fontSize="md" fontWeight="bold" textAlign="center">
|
<Text fontSize="md" fontWeight="bold" textAlign="center">
|
||||||
{match.awayTeam?.name}
|
{match.awayTeam?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Badge variant="subtle" colorPalette="orange" fontSize="2xs">
|
||||||
{t("away-team")}
|
{t("away-team")}
|
||||||
</Text>
|
</Badge>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</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.Body>
|
||||||
</Card.Root>
|
</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 */}
|
{/* Lineups Section */}
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
<LineupsCard match={match} prediction={prediction} />
|
<LineupsCard match={match} prediction={prediction} />
|
||||||
|
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
{/* Prediction Section */}
|
{/* Prediction Section */}
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
<Box>
|
<Box>
|
||||||
<Flex justify="space-between" align="center" mb={4}>
|
<Flex justify="space-between" align="center" mb={4}>
|
||||||
<Heading as="h2" size="lg">
|
<Heading as="h2" size="lg">
|
||||||
@@ -275,7 +385,9 @@ export default function MatchDetailContent() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
{/* Odds Section */}
|
{/* Odds Section */}
|
||||||
|
{/* ═══════════════════════════════════════════ */}
|
||||||
{match.odds && Object.keys(match.odds).length > 0 && (
|
{match.odds && Object.keys(match.odds).length > 0 && (
|
||||||
<OddsCard odds={match.odds} />
|
<OddsCard odds={match.odds} />
|
||||||
)}
|
)}
|
||||||
@@ -283,3 +395,88 @@ export default function MatchDetailContent() {
|
|||||||
</SlideUp>
|
</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";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import { SlideUp, FadeIn } from "@/components/motion";
|
import { SlideUp, FadeIn } from "@/components/motion";
|
||||||
import { useTeamById, useTeamMatches } from "@/lib/api/leagues/use-hooks";
|
import { useTeamById, useTeamMatches } from "@/lib/api/leagues/use-hooks";
|
||||||
import { LuArrowLeft, LuCalendar, LuTrophy, LuChevronDown } from "react-icons/lu";
|
import { LuArrowLeft, LuCalendar, LuTrophy, LuChevronDown } from "react-icons/lu";
|
||||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||||
import { useState, useMemo, useCallback } from "react";
|
import { useState, useMemo, useCallback } from "react";
|
||||||
|
import { LoginModal } from "@/components/auth/login-modal";
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────
|
||||||
// Utility Functions
|
// Utility Functions
|
||||||
@@ -32,7 +34,7 @@ function getMatchTimestamp(match: MatchResponseDto): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMatchStatus(match: MatchResponseDto): string {
|
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 {
|
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 {
|
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 {
|
function getLeagueLabel(match: MatchResponseDto): string {
|
||||||
@@ -83,6 +85,8 @@ export default function TeamDetailContent() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
|
|
||||||
const teamId = params.id as string;
|
const teamId = params.id as string;
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
@@ -245,7 +249,7 @@ export default function TeamDetailContent() {
|
|||||||
cardBg={cardBg}
|
cardBg={cardBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
statusBadge={getStatusBadge(match)}
|
statusBadge={getStatusBadge(match)}
|
||||||
onClick={() => router.push(`/tr/matches/${match.id}`)}
|
onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -316,7 +320,7 @@ export default function TeamDetailContent() {
|
|||||||
cardBg={cardBg}
|
cardBg={cardBg}
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
statusBadge={getStatusBadge(match)}
|
statusBadge={getStatusBadge(match)}
|
||||||
onClick={() => router.push(`/tr/matches/${match.id}`)}
|
onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -379,6 +383,13 @@ export default function TeamDetailContent() {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
|
|
||||||
|
{/* Login Modal — shown when unauthenticated user clicks a match */}
|
||||||
|
<LoginModal
|
||||||
|
open={loginModalOpen}
|
||||||
|
onOpenChange={setLoginModalOpen}
|
||||||
|
initialMode="login"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</SlideUp>
|
</SlideUp>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,31 +83,41 @@ export interface MatchResponseDto {
|
|||||||
odds?: Record<string, Record<string, { odd: string }>>;
|
odds?: Record<string, Record<string, { odd: string }>>;
|
||||||
|
|
||||||
// Nested Objects (from Backend include)
|
// Nested Objects (from Backend include)
|
||||||
homeTeam?: { name: string; logo?: string; [key: string]: unknown };
|
homeTeam?: { id?: string; name: string; logo?: string };
|
||||||
awayTeam?: { name: string; logo?: string; [key: string]: unknown };
|
awayTeam?: { id?: string; name: string; logo?: string };
|
||||||
league?: {
|
league?: {
|
||||||
name: string;
|
name: string;
|
||||||
country?: { name: string; flag?: string };
|
country?: { name: string; flag?: string };
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
};
|
||||||
lineups?: {
|
lineups?: {
|
||||||
home: Array<{
|
home: Array<{
|
||||||
player?: { name: string; id: string; [key: string]: unknown };
|
player?: { name: string; id: string };
|
||||||
position?: string | null;
|
position?: string | null;
|
||||||
shirtNumber?: number | null;
|
shirtNumber?: number | null;
|
||||||
isStarting?: boolean;
|
isStarting?: boolean;
|
||||||
[key: string]: unknown;
|
isProbable?: boolean;
|
||||||
|
lineupSource?: string;
|
||||||
|
projectionConfidence?: number;
|
||||||
}>;
|
}>;
|
||||||
away: Array<{
|
away: Array<{
|
||||||
player?: { name: string; id: string; [key: string]: unknown };
|
player?: { name: string; id: string };
|
||||||
position?: string | null;
|
position?: string | null;
|
||||||
shirtNumber?: number | null;
|
shirtNumber?: number | null;
|
||||||
isStarting?: boolean;
|
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 {
|
export interface ActiveLeagueDto {
|
||||||
|
|||||||
Reference in New Issue
Block a user