"use client";
import {
Box,
Flex,
Text,
Heading,
Badge,
VStack,
HStack,
Image,
Button,
Card,
SimpleGrid,
Skeleton,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp, FadeIn } from "@/components/motion";
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
import { usePrediction } from "@/lib/api/predictions/use-hooks";
import { useGetMe } from "@/lib/api/users/use-hooks";
import { useQueryClient } from "@tanstack/react-query";
import { UsersQueryKeys } from "@/lib/api/users/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,
LuUser,
LuSparkles,
LuInfo,
LuChevronDown,
LuChevronUp,
LuCalendar,
LuArrowRight,
} from "react-icons/lu";
import type {
MatchResponseDto,
MatchEvent,
} from "@/lib/api/matches/types";
// ─────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────
interface SidelinedPlayer {
playerName: string;
positionShort?: string;
description?: string;
type?: string;
matchesMissed?: number;
}
interface SidelinedTeam {
players?: SidelinedPlayer[];
}
interface SidelinedData {
homeTeam?: SidelinedTeam;
awayTeam?: SidelinedTeam;
}
// ─────────────────────────────────────────────────
// Skeleton Loading
// ─────────────────────────────────────────────────
function MatchDetailSkeleton() {
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
return (
{/* Header skeleton */}
{/* Tab bar skeleton */}
{/* Content skeletons */}
{[1, 2].map((i) => (
{[1, 2, 3, 4].map((j) => (
))}
))}
);
}
// ─────────────────────────────────────────────────
// Main Component
// ─────────────────────────────────────────────────
export default function MatchDetailContent() {
const t = useTranslations("matches");
const tPred = useTranslations("predictions");
const queryClient = useQueryClient();
const { data: meData } = useGetMe();
const usageLimit = meData?.data?.usageLimit;
const hasLimit = usageLimit
? usageLimit.maxAnalyses - usageLimit.analysisCount > 0
: true;
const tCommon = useTranslations("common");
const params = useParams();
const router = useRouter();
const matchId = params.id as string;
const { data: matchData, isLoading: matchLoading } = useMatchDetails(matchId);
const {
data: predictionData,
isLoading: predLoading,
refetch: refetchPredictionRaw,
isFetching: isPredFetching,
} = usePrediction(matchId);
const [officialsOpen, setOfficialsOpen] = useState(false);
const [activeTab, setActiveTab] = useState("all");
const refetchPrediction = async () => {
await refetchPredictionRaw();
queryClient.invalidateQueries({ queryKey: UsersQueryKeys.me() });
};
// Colors
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 tabBg = useColorModeValue("white", "gray.800");
const disclaimerBg = useColorModeValue("blue.50", "rgba(99,179,237,0.08)");
const disclaimerBorder = useColorModeValue("blue.100", "blue.800");
const timelineBg = useColorModeValue("gray.200", "gray.600");
const match = matchData?.data as MatchResponseDto | undefined;
const prediction = predictionData?.data;
if (matchLoading) return ;
if (!match) {
return (
{t("no-matches")}
);
}
// ── Derived state ──────────────────────────────
const isLive = match.status === "LIVE";
const isFinished =
match.status === "Finished" ||
match.status === "FT" ||
match.state === "postGame";
const winner = match.winner; // "home" | "away" | "draw" | undefined
const homeWon = winner === "home";
const awayWon = winner === "away";
const matchEvents = (match.events || match.playerEvents || [])
.slice()
.sort((a, b) => (parseInt(a.timeMinute) || 0) - (parseInt(b.timeMinute) || 0));
const homeStats = match.stats?.home;
const awayStats = match.stats?.away;
const hasStats =
homeStats != null &&
awayStats != null &&
(homeStats.possessionPercentage != null || homeStats.totalShots != null);
const officials = match.officials || [];
const mainReferee = officials.find((o) => o.roleId === 1);
const otherOfficials = officials.filter((o) => o.roleId !== 1);
const sidelined = (match.sidelined || {}) as SidelinedData;
const hasSidelined =
!isFinished &&
((sidelined.homeTeam?.players?.length || 0) > 0 ||
(sidelined.awayTeam?.players?.length || 0) > 0);
const show = (key: string) => activeTab === "all" || activeTab === key;
const tabs = [
{ key: "all", label: t("all-matches", { defaultValue: "Hepsi" }) },
...(matchEvents.length > 0 ? [{ key: "events", label: t("match-events") }] : []),
...(hasStats ? [{ key: "stats", label: t("statistics") }] : []),
{ key: "lineups", label: t("lineups") },
{ key: "prediction", label: tPred("title") },
...((match.odds && Object.keys(match.odds).length > 0) ? [{ key: "odds", label: t("odds") }] : []),
];
return (
{/* Back Button */}
{/* ══════════════════════════════════════════════ */}
{/* 1. MATCH HEADER */}
{/* ══════════════════════════════════════════════ */}
{/* League banner */}
{match.league && (
{match.league.country?.flag && (
)}
{match.league.country?.name && `${match.league.country.name} · `}
{match.league.name}
{isLive && (
)}
{isLive ? t("live") : isFinished ? t("finished") : t("not-started")}
)}
{/* Teams & Score */}
{/* Home Team */}
{match.homeTeam?.logo ? (
) : (
{match.homeTeam?.name?.charAt(0) || "H"}
)}
{match.homeTeam?.name}
{t("home-team")}
{/* Score */}
{match.score && (isLive || isFinished) ? (
<>
{/* FT badge */}
{isFinished && (
FT
)}
{match.score.home}
:
{match.score.away}
{match.score.htHome != null && match.score.htAway != null && (
({t("half-time")}: {match.score.htHome}–{match.score.htAway})
)}
>
) : (
{t("vs")}
)}
{/* Date */}
{new Date(match.mstUtc).toLocaleDateString("tr-TR", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
{/* Away Team */}
{match.awayTeam?.logo ? (
) : (
{match.awayTeam?.name?.charAt(0) || "A"}
)}
{match.awayTeam?.name}
{t("away-team")}
{/* Referee + Officials (collapsible) */}
{(mainReferee || officials.length > 0) && (
0 ? "pointer" : "default"}
onClick={() => otherOfficials.length > 0 && setOfficialsOpen((o) => !o)}
_hover={otherOfficials.length > 0 ? { opacity: 0.75 } : {}}
>
{t("main-referee")}:{" "}
{mainReferee?.name ?? t("referee")}
{otherOfficials.length > 0 && (
{officialsOpen ? : }
)}
{/* Other officials expanded */}
{officialsOpen && otherOfficials.length > 0 && (
{otherOfficials.map((official) => (
{official.name}
{officialRoleLabel(official.roleId, t)}
))}
)}
)}
{/* ══════════════════════════════════════════════ */}
{/* 2. TAB FILTER BAR */}
{/* ══════════════════════════════════════════════ */}
{tabs.map((tab) => {
const isActive = activeTab === tab.key;
return (
);
})}
{/* ══════════════════════════════════════════════ */}
{/* 3. INJURIES (only pre-match) */}
{/* ══════════════════════════════════════════════ */}
{hasSidelined && show("all") && (
🏥 {t("sidelined")}
)}
{/* ══════════════════════════════════════════════ */}
{/* 4. MATCH EVENTS TIMELINE */}
{/* ══════════════════════════════════════════════ */}
{matchEvents.length > 0 && show("events") && (
{t("match-events")}
{/* Team name headers */}
{match.homeTeam?.name}
{match.awayTeam?.name}
{/* Events */}
{/* Center vertical line */}
{matchEvents.map((event) => (
))}
)}
{/* ══════════════════════════════════════════════ */}
{/* 5. TEAM STATISTICS */}
{/* ══════════════════════════════════════════════ */}
{hasStats && show("stats") && (
{/* Header with team names */}
{match.homeTeam?.name}
{t("statistics")}
{match.awayTeam?.name}
{homeStats!.possessionPercentage != null && (
)}
{homeStats!.totalShots != null && (
)}
{homeStats!.shotsOnTarget != null && (
)}
{homeStats!.shotsOffTarget != null && (
)}
{homeStats!.totalPasses != null && (
)}
{homeStats!.corners != null && (
)}
{homeStats!.fouls != null && (
)}
{homeStats!.offsides != null && (
)}
)}
{/* ══════════════════════════════════════════════ */}
{/* 6. LINEUPS */}
{/* ══════════════════════════════════════════════ */}
{show("lineups") && (
)}
{/* ══════════════════════════════════════════════ */}
{/* 7. PREDICTION */}
{/* ══════════════════════════════════════════════ */}
{show("prediction") &&
{tPred("title")}
{/* Pre-match disclaimer */}
{tPred("pre-match-disclaimer")}
{predLoading || isPredFetching ? (
) : prediction ? (
) : (
{tPred("no-predictions")}
{!hasLimit && (
{tCommon("limits.out_of_analysis")}
)}
)}
}
{/* ══════════════════════════════════════════════ */}
{/* 8. ODDS */}
{/* ══════════════════════════════════════════════ */}
{match.odds && Object.keys(match.odds).length > 0 && show("odds") && (
)}
);
}
// ─────────────────────────────────────────────────
// Timeline Event Row
// ─────────────────────────────────────────────────
interface TimelineEventRowProps {
event: MatchEvent;
homeTeamId?: string;
t: ReturnType;
borderColor: string;
subtleBg: string;
timelineBg: string;
}
function TimelineEventRow({
event,
homeTeamId,
t,
subtleBg,
}: TimelineEventRowProps) {
const isHome = event.teamId === homeTeamId || event.position === "home";
const isGoal = event.eventType === "goal";
const isCard = event.eventType === "card";
const isSub = event.eventType === "substitute";
const icon = (() => {
if (isGoal) return "⚽";
if (isCard && event.eventSubtype === "yc") return "🟨";
if (isCard && (event.eventSubtype === "rc" || event.eventSubtype === "y2c")) return "🟥";
if (isSub) return "🔄";
return "•";
})();
const mainText = (() => {
if (isGoal) {
const penalty = event.eventSubtype === "penalty-goal" ? ` (${t("penalty")})` : "";
return `${event.player?.name || ""}${penalty}`;
}
if (isSub) return event.player?.name || "";
return event.player?.name || "";
})();
const subText = (() => {
if (isGoal && event.assistPlayer)
return `${t("assist")}: ${event.assistPlayer.name}`;
if (isSub && event.substitutedOut)
return `↓ ${event.substitutedOut.name}`;
if (isCard) return event.eventSubtype === "yc" ? t("yellow-card") : t("red-card");
return null;
})();
return (
{/* Home side */}
{isHome && (
{mainText}
{subText && (
{subText}
)}
{icon}
{isGoal && event.scoreAfter && (
{event.scoreAfter}
)}
)}
{/* Center minute bubble */}
{event.timeMinute}'
{/* Away side */}
{!isHome && (
{icon}
{mainText}
{subText && (
{subText}
)}
{isGoal && event.scoreAfter && (
{event.scoreAfter}
)}
)}
);
}
// ─────────────────────────────────────────────────
// Stat Bar
// ─────────────────────────────────────────────────
interface StatBarProps {
label: string;
homeVal: number;
awayVal: number;
suffix?: string;
}
function StatBar({ label, homeVal, awayVal, suffix = "" }: StatBarProps) {
const total = homeVal + awayVal || 1;
const homePct = (homeVal / total) * 100;
const awayPct = (awayVal / total) * 100;
const homeBarBg = useColorModeValue("blue.400", "blue.500");
const awayBarBg = useColorModeValue("orange.400", "orange.500");
const trackBg = useColorModeValue("gray.100", "gray.700");
const homeWins = homeVal > awayVal;
const awayWins = awayVal > homeVal;
return (
{homeVal}
{suffix}
{homeWins && }
{label}
{awayWins && (
)}
{awayVal}
{suffix}
);
}
// ─────────────────────────────────────────────────
// Sidelined Column
// ─────────────────────────────────────────────────
interface SidelinedColumnProps {
team?: SidelinedTeam;
teamName: string;
teamLogo?: string;
injuryBg: string;
injuryBorder: string;
t: ReturnType;
}
function SidelinedColumn({
team,
teamName,
teamLogo,
injuryBg,
injuryBorder,
t,
}: SidelinedColumnProps) {
const players = team?.players || [];
if (players.length === 0) {
return (
{teamLogo && (
)}
{teamName}
{t("no-sidelined")}
);
}
return (
{teamLogo && (
)}
{teamName}
{players.length}
{players.map((player, idx) => (
{player.playerName}
{player.positionShort && (
{player.positionShort}
)}
{player.description ||
(player.type === "injury"
? t("injury")
: player.type === "suspended"
? t("suspended")
: t("other-reason"))}
{player.matchesMissed !== undefined && player.matchesMissed > 0 && (
{player.matchesMissed} {t("matches-missed")}
)}
))}
);
}
// ─────────────────────────────────────────────────
// Official Role Label
// ─────────────────────────────────────────────────
function officialRoleLabel(
roleId: number,
t: ReturnType,
): string {
switch (roleId) {
case 1: return t("main-referee");
case 2: return t("assistant-referee");
case 3: return t("fourth-official");
case 5: return t("var-referee");
case 6: return t("avar-referee");
default: return t("referee");
}
}