diff --git a/app/live-detail/[id].tsx b/app/live-detail/[id].tsx
index 6219dcd..b9ec88b 100644
--- a/app/live-detail/[id].tsx
+++ b/app/live-detail/[id].tsx
@@ -5,6 +5,9 @@ import { LiveScoreHeader } from "@/components/live-detail/live-score-header";
import { OddsCard } from "@/components/live-detail/odds-card";
import { OtherInfoCard } from "@/components/live-detail/other-info-card";
import { StatsCard } from "@/components/live-detail/stats-card";
+import { TennisPowerGraph } from "@/components/live-detail/tennis-power-graph";
+import { TennisScoreboard } from "@/components/live-detail/tennis-scoreboard";
+import { TennisStatsCard } from "@/components/live-detail/tennis-stats-card";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { Colors } from "@/constants/theme";
@@ -99,14 +102,37 @@ export default function LiveDetailScreen() {
const renderTabContent = () => {
const numericSportId = parseInt(sport_id || "1");
+ // Tennis Check
+ const isTennis =
+ !!match.event_first_player ||
+ (match.league_name &&
+ /ATP|WTA|ITF|Challenger/i.test(match.league_name || ""));
+
switch (activeTab) {
case "stats":
- return ;
+ return !isTennis ? (
+
+ ) : (
+
+
+ {t("detail.empty_stats")}
+
+
+ );
case "odds":
return (
);
case "detail":
+ if (isTennis) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
return (
<>
diff --git a/components/live-detail/live-score-header.tsx b/components/live-detail/live-score-header.tsx
index f5b2071..c69d854 100644
--- a/components/live-detail/live-score-header.tsx
+++ b/components/live-detail/live-score-header.tsx
@@ -151,9 +151,24 @@ export function LiveScoreHeader({ match, topInset }: LiveScoreHeaderProps) {
};
const lastServerMatchRef = React.useRef(
- `${match.event_status}-${match.event_time}`
+ `${match.event_status}-${match.event_time}`,
);
+ // Tennis Logic
+ const isTennis =
+ !!match.event_first_player ||
+ (match.league_name &&
+ /ATP|WTA|ITF|Challenger/i.test(match.league_name || ""));
+
+ const homeName = isTennis ? match.event_first_player : match.event_home_team;
+ const awayName = isTennis ? match.event_second_player : match.event_away_team;
+ const homeLogo = isTennis
+ ? match.event_first_player_logo
+ : match.home_team_logo;
+ const awayLogo = isTennis
+ ? match.event_second_player_logo
+ : match.away_team_logo;
+
// 服务器时间同步
React.useEffect(() => {
const currentKey = `${match.event_status}-${match.event_time}`;
@@ -251,13 +266,13 @@ export function LiveScoreHeader({ match, topInset }: LiveScoreHeaderProps) {
- {match.event_home_team}
+ {homeName}
@@ -274,10 +289,12 @@ export function LiveScoreHeader({ match, topInset }: LiveScoreHeaderProps) {
{match.event_status}
)}
- {displayTime}
+ {!isTennis && (
+ {displayTime}
+ )}
- {match.goalscorers && match.goalscorers.length > 0 && (
+ {!isTennis && match.goalscorers && match.goalscorers.length > 0 && (
@@ -286,14 +303,24 @@ export function LiveScoreHeader({ match, topInset }: LiveScoreHeaderProps) {
)}
+
+ {isTennis && match.event_serve && (
+
+
+
+ {match.event_serve === "First Player" ? homeName : awayName}{" "}
+ Serving
+
+
+ )}
- {match.event_away_team}
+ {awayName}
@@ -442,4 +469,21 @@ const styles = StyleSheet.create({
fontSize: 10,
marginLeft: 4,
},
+ serveIndicator: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginTop: 6,
+ backgroundColor: "rgba(0,0,0,0.3)",
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 10,
+ gap: 4,
+ },
+ serveText: {
+ color: "#FFF",
+ fontSize: 10,
+ },
+ tennisAvatar: {
+ borderRadius: 30,
+ },
});
diff --git a/components/live-detail/stats-card.tsx b/components/live-detail/stats-card.tsx
index 90003b9..28557a1 100644
--- a/components/live-detail/stats-card.tsx
+++ b/components/live-detail/stats-card.tsx
@@ -87,12 +87,13 @@ export function StatsCard({ match, isDark }: StatsCardProps) {
const progressPercent = Math.min(100, (totalSeconds / (90 * 60)) * 100);
// 从 statistics 中提取数据
- const stats = match.statistics || [];
+ const stats = (match.statistics || []) as any[];
const getStatValue = (type: string) => {
const stat = stats.find((s) => s.type === type);
- return stat
- ? { home: stat.home, away: stat.away }
- : { home: "0", away: "0" };
+ if (stat && stat.home) {
+ return { home: stat.home, away: stat.away };
+ }
+ return { home: "0", away: "0" };
};
const possession = getStatValue("Ball Possession");
diff --git a/components/live-detail/tennis-power-graph.tsx b/components/live-detail/tennis-power-graph.tsx
new file mode 100644
index 0000000..a29f450
--- /dev/null
+++ b/components/live-detail/tennis-power-graph.tsx
@@ -0,0 +1,332 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { LiveScoreMatch } from "@/types/api";
+import React, { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { Image, ScrollView, StyleSheet, View } from "react-native";
+
+interface TennisPowerGraphProps {
+ match: LiveScoreMatch;
+ isDark: boolean;
+}
+
+export function TennisPowerGraph({ match, isDark }: TennisPowerGraphProps) {
+ const { t } = useTranslation();
+ const pointData = match.pointbypoint;
+
+ if (!pointData || pointData.length === 0) {
+ return null;
+ }
+
+ // Grouped Data by Game
+ // Structure: { gameIndex: number, score: string, set: string, points: {winner: 1|2}[] }
+ const gameData = useMemo(() => {
+ return pointData.map((game) => {
+ const points = game.points || [];
+ const bars: { winner: 1 | 2 }[] = [];
+ let prevP1Score = 0;
+ let prevP2Score = 0;
+
+ const parseScore = (s: string) => {
+ if (s === "A") return 45;
+ return parseInt(s) || 0;
+ };
+
+ points.forEach((point: { score: string }) => {
+ const parts = point.score.split("-").map((s) => s.trim());
+ if (parts.length !== 2) return;
+ const p1 = parseScore(parts[0]);
+ const p2 = parseScore(parts[1]);
+
+ let winner: 1 | 2 | null = null;
+ if (p1 > prevP1Score && p2 === prevP2Score) winner = 1;
+ else if (p2 > prevP2Score && p1 === prevP1Score) winner = 2;
+ else if (p1 === 45 && prevP1Score === 40) winner = 1;
+ else if (p2 === 45 && prevP2Score === 40) winner = 2;
+ else if (p1 === 40 && prevP1Score === 45) winner = 2;
+ else if (p2 === 40 && prevP2Score === 45) winner = 1;
+ else if (
+ points.length === 1 &&
+ prevP1Score === 0 &&
+ prevP2Score === 0
+ ) {
+ if (p1 > 0) winner = 1;
+ if (p2 > 0) winner = 2;
+ } else if (p1 > prevP1Score || (p2 < prevP2Score && prevP2Score === 45))
+ winner = 1;
+ else if (p2 > prevP2Score || (p1 < prevP1Score && prevP1Score === 45))
+ winner = 2;
+
+ if (winner) {
+ bars.push({ winner });
+ }
+ prevP1Score = p1;
+ prevP2Score = p2;
+ });
+
+ // Find if this game ended a set? Or just carry current set score/game score.
+ // `game.score` is e.g. "1 - 0" (Set Score or Game Score within set?)
+ // Usually `pointbypoint` item has `score` field which is the score in Games for that Set.
+
+ return {
+ gameIndex: parseInt(game.number_game),
+ setNumber: game.set_number, // "Set 1"
+ score: game.score, // "6 - 4" or "1 - 0"
+ serveWinner: game.serve_winner, // "First Player" or "Second Player"
+ bars,
+ };
+ });
+ }, [pointData]);
+
+ if (gameData.length === 0) return null;
+
+ // Render logic
+ // Left side: Avatars
+ // Right side: ScrollView of Game Blocks
+
+ // Avatars
+ const p1Logo = match.event_first_player_logo;
+ const p2Logo = match.event_second_player_logo;
+
+ return (
+
+
+ {t("detail.power_graph")}
+
+
+
+ {/* Sidebar - Avatars */}
+
+ {/* Spacer top */}
+
+ {p1Logo ? (
+
+ ) : (
+
+ )}
+
+ {" "}
+ {/* Gap corresponding to center line if needed, but styling is easier with even spacing */}
+
+ {p2Logo ? (
+
+ ) : (
+
+ )}
+
+ {/* Spacer bottom */}
+
+
+ {/* Scrollable Graph */}
+
+ {gameData.map((game, i) => {
+ // Updated to match screenshot: Split background (Blue top, Yellow bottom)
+ const topBg = isDark ? "rgba(33, 150, 243, 0.15)" : "#E3F2FD";
+ const botBg = isDark ? "rgba(255, 193, 7, 0.15)" : "#FFF8E1";
+
+ return (
+
+ {/* Top Score Label if needed */}
+
+
+ {/* Bars Area */}
+
+ {/* Backgrounds */}
+
+
+
+ {/* Center Line */}
+
+
+ {game.bars.map((bar, bi) => (
+
+ {/* P1 Bar (Top) */}
+
+ {bar.winner === 1 && (
+
+ )}
+
+
+ {/* P2 Bar (Bottom) */}
+
+ {bar.winner === 2 && (
+
+ )}
+
+
+ ))}
+
+
+ {/* Bottom Game Number */}
+
+
+ {game.gameIndex}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ margin: 16,
+ padding: 16,
+ borderRadius: 12,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: "bold",
+ marginBottom: 20,
+ },
+ contentRow: {
+ flexDirection: "row",
+ height: 120, // Total height for graph area
+ },
+ sidebar: {
+ width: 40,
+ alignItems: "center",
+ justifyContent: "center", // Center avatars vertically relative to bars
+ paddingBottom: 20, // Adjust for game number row
+ },
+ avatarContainer: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ overflow: "hidden",
+ borderWidth: 1,
+ borderColor: "#333",
+ },
+ avatar: {
+ width: "100%",
+ height: "100%",
+ },
+ graphScroll: {
+ flex: 1,
+ marginLeft: 8,
+ },
+ gameBlock: {
+ paddingHorizontal: 4,
+ marginRight: 2,
+ minWidth: 40,
+ borderRadius: 4,
+ },
+ gameTopLabel: {
+ height: 20,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ barsArea: {
+ flexDirection: "row",
+ alignItems: "center",
+ height: 60, // Central bar area
+ position: "relative",
+ justifyContent: "center",
+ },
+ centerLine: {
+ position: "absolute",
+ width: "100%",
+ height: 1,
+ top: 30, // Middle of 60
+ },
+ barColumn: {
+ width: 6,
+ marginHorizontal: 1,
+ height: "100%",
+ justifyContent: "center",
+ },
+ barSegment: {
+ width: "100%",
+ },
+ activeBar: {
+ width: "100%",
+ borderRadius: 2,
+ },
+ gameBottomLabel: {
+ height: 20,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ gameNumberText: {
+ fontSize: 10,
+ opacity: 0.6,
+ },
+});
diff --git a/components/live-detail/tennis-scoreboard.tsx b/components/live-detail/tennis-scoreboard.tsx
new file mode 100644
index 0000000..7dd2b4b
--- /dev/null
+++ b/components/live-detail/tennis-scoreboard.tsx
@@ -0,0 +1,244 @@
+import { ThemedText } from "@/components/themed-text";
+import { LiveScoreMatch } from "@/types/api";
+import { Image } from "expo-image";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { StyleSheet, View } from "react-native";
+
+interface TennisScoreboardProps {
+ match: LiveScoreMatch;
+ isDark: boolean;
+}
+
+export function TennisScoreboard({ match, isDark }: TennisScoreboardProps) {
+ const { t } = useTranslation();
+ // Tennis Logic
+ const isTennis =
+ !!match.event_first_player ||
+ (match.league_name &&
+ /ATP|WTA|ITF|Challenger/i.test(match.league_name || ""));
+
+ if (!isTennis) return null;
+
+ const homeName = isTennis ? match.event_first_player : match.event_home_team;
+ const awayName = isTennis ? match.event_second_player : match.event_away_team;
+ const homeLogo = isTennis
+ ? match.event_first_player_logo
+ : match.home_team_logo;
+ const awayLogo = isTennis
+ ? match.event_second_player_logo
+ : match.away_team_logo;
+
+ const tennisScores = React.useMemo(() => {
+ if (!match.scores) return [];
+ try {
+ const s = match.scores;
+ return Array.isArray(s) ? s : [];
+ } catch {
+ return [];
+ }
+ }, [match.scores]);
+
+ const gamePoints = React.useMemo(() => {
+ if (!match.event_game_result) return { p1: "0", p2: "0" };
+ const parts = match.event_game_result.split("-");
+ if (parts.length === 2) {
+ return { p1: parts[0].trim(), p2: parts[1].trim() };
+ }
+ return { p1: "0", p2: "0" };
+ }, [match.event_game_result]);
+
+ const totalSets = React.useMemo(() => {
+ if (!match.event_final_result) return { p1: 0, p2: 0 };
+ const parts = match.event_final_result.split("-");
+ if (parts.length === 2) {
+ return {
+ p1: parseInt(parts[0].trim()) || 0,
+ p2: parseInt(parts[1].trim()) || 0,
+ };
+ }
+ return { p1: 0, p2: 0 };
+ }, [match.event_final_result]);
+
+ const bgColor = isDark ? "#1E1E20" : "#FFF";
+ const textColor = isDark ? "#FFF" : "#000";
+ const headerColor = isDark ? "#888" : "#666";
+ const borderColor = isDark ? "#333" : "#F0F0F0";
+
+ return (
+
+
+
+ {t("detail.scoreboard", "Scoreboard")}
+
+
+
+
+ Player
+
+
+ Game
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ S{i}
+
+ ))}
+
+ Sets
+
+
+
+
+
+
+
+
+ {gamePoints.p1}
+
+ {[1, 2, 3, 4, 5].map((i) => {
+ const setScore = tennisScores.find((s: any) => s.score_set == i);
+ return (
+
+ {setScore ? setScore.score_first : "-"}
+
+ );
+ })}
+
+ {totalSets.p1}
+
+
+
+
+
+
+
+
+ {gamePoints.p2}
+
+ {[1, 2, 3, 4, 5].map((i) => {
+ const setScore = tennisScores.find((s: any) => s.score_set == i);
+ return (
+
+ {setScore ? setScore.score_second : "-"}
+
+ );
+ })}
+
+ {totalSets.p2}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ tennisBoardContainer: {
+ marginHorizontal: 16,
+ borderRadius: 16,
+ padding: 16,
+ marginTop: 12,
+ marginBottom: 16,
+ shadowColor: "#000",
+ shadowOpacity: 0.05,
+ shadowRadius: 10,
+ elevation: 2,
+ },
+ header: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ },
+ tennisBoardHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderBottomWidth: 1,
+ paddingBottom: 8,
+ marginBottom: 8,
+ },
+ boardHeaderLabel: {
+ fontSize: 12,
+ fontWeight: "500",
+ },
+ boardPlayerCol: {
+ width: 40,
+ alignItems: "center",
+ },
+ boardColCenter: {
+ flex: 1,
+ textAlign: "center",
+ },
+ tennisRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ height: 36,
+ },
+ miniAvatar: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ },
+ boardVal: {
+ fontSize: 14,
+ fontWeight: "600",
+ },
+});
diff --git a/components/live-detail/tennis-stats-card.tsx b/components/live-detail/tennis-stats-card.tsx
new file mode 100644
index 0000000..ead4012
--- /dev/null
+++ b/components/live-detail/tennis-stats-card.tsx
@@ -0,0 +1,408 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { LiveScoreMatch } from "@/types/api";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { StyleSheet, View } from "react-native";
+import Svg, { Circle, G } from "react-native-svg";
+
+interface TennisStatsCardProps {
+ match: LiveScoreMatch;
+ isDark: boolean;
+}
+
+// Helper for Circular Progress
+// Updated to match screenshot: Labels on top, circle in middle, values on sides.
+const CircularStat = ({
+ value1,
+ value2,
+ label,
+ color1 = "#2196F3", // Blue
+ color2 = "#FFC107", // Gold
+ size = 50,
+ strokeWidth = 5,
+ isDark,
+}: {
+ value1: string; // "67%" or "67"
+ value2: string;
+ label: string;
+ color1?: string;
+ color2?: string;
+ size?: number;
+ strokeWidth?: number;
+ isDark: boolean;
+}) => {
+ const parse = (v: string) => parseFloat(v.replace("%", "")) || 0;
+ const v1 = parse(value1);
+ const v2 = parse(value2);
+
+ // Radius
+ const r = (size - strokeWidth) / 2;
+ const c = size / 2;
+ const circumference = 2 * Math.PI * r;
+
+ // Inner radius
+ const innerR = r - strokeWidth - 2; // small gap
+ const innerCircumference = 2 * Math.PI * innerR;
+
+ // Colors for text need to be readable on white background if card is always white
+ // Screenshot shows white card on dark background.
+ // BUT the text "First Serve %" is dark/grey.
+ // The values 67.4 are Blue/Gold.
+ // The circle tracks are light grey maybe.
+
+ const trackColor = isDark ? "#333" : "#E0E0E0";
+ const labelColor = isDark ? "#AAA" : "#666";
+
+ return (
+
+ {/* Label centered */}
+
+ {label}
+
+
+
+ {/* Left Value P1 */}
+
+ {value1}
+
+
+ {/* Circle Graphic */}
+
+
+
+
+ {/* Right Value P2 */}
+
+ {value2}
+
+
+
+ );
+};
+
+// Helper for Bar Stat
+const BarStat = ({
+ label,
+ value1,
+ value2,
+ color1 = "#2196F3",
+ color2 = "#FFC107",
+ isDark,
+}: {
+ label: string;
+ value1: string;
+ value2: string;
+ color1?: string;
+ color2?: string;
+ isDark: boolean;
+}) => {
+ // Parse to number for bars
+ const v1 = parseFloat(value1) || 0;
+ const v2 = parseFloat(value2) || 0;
+ const total = v1 + v2 || 1;
+ const p1 = (v1 / total) * 100;
+ const p2 = (v2 / total) * 100;
+
+ return (
+
+
+
+ {value1}
+
+ {label}
+
+
+ {value2}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export function TennisStatsCard({ match, isDark }: TennisStatsCardProps) {
+ const { t } = useTranslation();
+ const statistics = match.statistics;
+
+ if (!statistics || !Array.isArray(statistics) || statistics.length === 0) {
+ return null;
+ }
+
+ // Check structure. If it's the Football type (home/away keys), we shouldn't be here (LiveDetail logic checks sport).
+ // But safely handle:
+ const isTennisFormat =
+ "player_key" in statistics[0] || "name" in statistics[0];
+
+ if (!isTennisFormat) return null;
+
+ // Group by name
+ // Map Name -> { p1Value: string, p2Value: string }
+ const statsMap: Record =
+ {};
+
+ // Assume P1 is first_player_key
+ // Using home_team_key as fallback/primary since first_player_key isn't in type definition
+ // In many APIs, for tennis, home_team_key === first_player_key
+ const p1Key = match.home_team_key;
+
+ // In generic response, keys might be strings vs numbers.
+ // Let's rely on finding two entries for same "name" and "period".
+ // Or just iterate.
+
+ (statistics as any[]).forEach((stat) => {
+ // Filter for 'match' period (total)
+ if (stat.period !== "match") return;
+
+ const name = stat.name;
+ if (!statsMap[name]) statsMap[name] = { p1: "0", p2: "0", type: stat.type };
+
+ // Assign to P1 or P2
+ // If we know p1Key
+ if (p1Key && stat.player_key == p1Key) {
+ statsMap[name].p1 = stat.value;
+ } else {
+ statsMap[name].p2 = stat.value;
+ }
+ });
+
+ // Define intended stats to show
+ // Top (Circles):
+ // "1st serve percentage" (1st Serve %)
+ // "1st serve points won" (1st Serve Win %)
+ // "Break Points Converted" -> "Break Success %" (Screenshot: 破发成功率)
+ // Or match keys exactly: "1st serve percentage", "1st serve points won", "Break Points Converted"
+
+ // Middle (Bars):
+ // "Aces" (Screenshot: 发球得分? Or implies Aces).
+ // "Double Faults" (Screenshot: 双误)
+ // "Service Points Won"
+
+ const circleStats = [
+ { key: "1st serve percentage", label: t("detail.stats.first_serve") },
+ {
+ key: "1st serve points won",
+ label: t("detail.stats.first_serve_points"),
+ },
+ { key: "Break Points Converted", label: t("detail.stats.break_points") },
+ ];
+
+ const barStats = [
+ { key: "Aces", label: t("detail.stats.aces") },
+ { key: "Double Faults", label: t("detail.stats.double_faults") },
+ ];
+
+ // Check if any of the target stats actually exist in the map
+ const hasAnyStat = [...circleStats, ...barStats].some((s) => statsMap[s.key]);
+ if (!hasAnyStat) return null;
+
+ return (
+
+
+ {t("detail.statistics")}
+
+
+
+ {circleStats.map((s) =>
+ statsMap[s.key] ? (
+
+ ) : null,
+ )}
+
+
+
+ {barStats.map((s) =>
+ statsMap[s.key] ? (
+
+ ) : null,
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ margin: 16,
+ padding: 16,
+ borderRadius: 12,
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: "bold",
+ marginBottom: 20,
+ },
+ circlesRow: {
+ flexDirection: "row",
+ justifyContent: "space-around",
+ marginBottom: 24,
+ },
+ circularStatContainer: {
+ alignItems: "center",
+ width: "30%",
+ },
+ circularLabel: {
+ fontSize: 12,
+ textAlign: "center",
+ opacity: 0.7,
+ height: 32, // Fixed height for alignment
+ },
+ circularRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ statValueSide: {
+ fontSize: 12,
+ fontWeight: "bold",
+ },
+ statValue: {
+ fontSize: 12,
+ fontWeight: "600",
+ },
+ barsColumn: {
+ gap: 16,
+ },
+ barStatContainer: {
+ marginBottom: 8,
+ },
+ barHeader: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 8,
+ },
+ pill: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 12,
+ minWidth: 40,
+ alignItems: "center",
+ },
+ pillText: {
+ color: "white",
+ fontWeight: "bold",
+ fontSize: 12,
+ },
+ barLabel: {
+ opacity: 0.8,
+ },
+ barTrack: {
+ flexDirection: "row",
+ height: 6,
+ backgroundColor: "#333",
+ borderRadius: 3,
+ overflow: "hidden",
+ },
+ barFill: {
+ height: "100%",
+ },
+});
diff --git a/components/match-card.tsx b/components/match-card.tsx
index 160b080..abda1a6 100644
--- a/components/match-card.tsx
+++ b/components/match-card.tsx
@@ -262,14 +262,15 @@ export function MatchCard({
return { home: "", away: "" };
}
- const corners = liveDetail.statistics.find((s) => s.type === "Corners");
+ // Cast statistics to any[] to avoid union type issues when accessing 'home'/'away' properties
+ const stats = liveDetail.statistics as any[];
+
+ const corners = stats.find((s) => s.type === "Corners");
if (corners) {
return { home: corners.home, away: corners.away };
}
- const dangerousAttacks = liveDetail.statistics.find(
- (s) => s.type === "Dangerous Attacks",
- );
+ const dangerousAttacks = stats.find((s) => s.type === "Dangerous Attacks");
if (dangerousAttacks) {
return { home: dangerousAttacks.home, away: dangerousAttacks.away };
}
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index fe0d285..0151eda 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -91,6 +91,18 @@
"retry": "Retry",
"fetch_failed": "Failed to fetch details",
"not_found": "Match data not found",
+ "scoreboard": "Scoreboard",
+ "power_graph": "Power Graph",
+ "statistics": "Statistics",
+ "stats": {
+ "first_serve": "1st Serve %",
+ "first_serve_points": "1st Serve Pts Won",
+ "break_points": "Break Success %",
+ "aces": "Aces",
+ "double_faults": "Double Faults",
+ "service_points": "Service Points Won",
+ "total_points": "Total Points Won"
+ },
"tabs": {
"info": "Details",
"stats": "Statistics",
diff --git a/i18n/locales/hi.json b/i18n/locales/hi.json
index 1cd85fe..32d81a8 100644
--- a/i18n/locales/hi.json
+++ b/i18n/locales/hi.json
@@ -96,6 +96,7 @@
"h2h": "आमने-सामने",
"chat": "चैट"
},
+ "scoreboard": "स्कोरबोर्ड",
"info_card": {
"title": "मैच जानकारी",
"country": "देश/क्षेत्र",
diff --git a/i18n/locales/id.json b/i18n/locales/id.json
index 05fdc9c..123143c 100644
--- a/i18n/locales/id.json
+++ b/i18n/locales/id.json
@@ -87,6 +87,7 @@
"retry": "Coba lagi",
"fetch_failed": "Gagal mengambil data",
"not_found": "Data pertandingan tidak ditemukan",
+ "scoreboard": "Papan Skor",
"tabs": {
"info": "Detail",
"stats": "Statistik",
diff --git a/i18n/locales/ms.json b/i18n/locales/ms.json
index fb7df10..d4fb244 100644
--- a/i18n/locales/ms.json
+++ b/i18n/locales/ms.json
@@ -87,6 +87,7 @@
"retry": "Cuba semula",
"fetch_failed": "Gagal mendapatkan maklumat",
"not_found": "Data perlawanan tidak ditemui",
+ "scoreboard": "Papan Skor",
"tabs": {
"info": "Maklumat",
"stats": "Statistik",
diff --git a/i18n/locales/th.json b/i18n/locales/th.json
index 7b16919..69b3634 100644
--- a/i18n/locales/th.json
+++ b/i18n/locales/th.json
@@ -87,6 +87,7 @@
"retry": "ลองอีกครั้ง",
"fetch_failed": "ไม่สามารถดึงข้อมูลได้",
"not_found": "ไม่พบข้อมูลการแข่งขัน",
+ "scoreboard": "สกอร์บอร์ด",
"tabs": {
"info": "รายละเอียด",
"stats": "สถิติ",
diff --git a/i18n/locales/vi.json b/i18n/locales/vi.json
index e79cbbb..a6ff878 100644
--- a/i18n/locales/vi.json
+++ b/i18n/locales/vi.json
@@ -87,6 +87,7 @@
"retry": "Thử lại",
"fetch_failed": "Không thể tải dữ liệu",
"not_found": "Không tìm thấy dữ liệu trận đấu",
+ "scoreboard": "Bảng điểm",
"tabs": {
"info": "Thông tin",
"stats": "Thống kê",
diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json
index 478afcf..94013e8 100644
--- a/i18n/locales/zh.json
+++ b/i18n/locales/zh.json
@@ -91,6 +91,18 @@
"retry": "重试",
"fetch_failed": "获取详情失败",
"not_found": "未找到比赛数据",
+ "scoreboard": "记分牌",
+ "power_graph": "功率图",
+ "statistics": "统计数据",
+ "stats": {
+ "first_serve": "一发成功率",
+ "first_serve_points": "一发得分率",
+ "break_points": "破发成功率",
+ "aces": "Ace球",
+ "double_faults": "双误",
+ "service_points": "发球得分",
+ "total_points": "总得分"
+ },
"tabs": {
"info": "详情",
"stats": "统计数据",
diff --git a/lib/api.ts b/lib/api.ts
index 5772e33..448754c 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -167,7 +167,7 @@ export const fetchTodayMatches = async (options: {
const year = options.date.getFullYear();
const month = String(options.date.getMonth() + 1).padStart(2, "0");
const day = String(options.date.getDate()).padStart(2, "0");
- params.date = `${year}-${month}-${day}`;
+ params.date = `${year}-${month}-${day}`;
} else {
params.date = options.date;
}
@@ -214,11 +214,11 @@ export const fetchLiveScore = async (
leagueId?: number,
timezone?: string,
): Promise => {
- // console.log("Fetching live scores with params:", {
- // sportId,
- // leagueId,
- // timezone,
- // });
+ console.log("Fetching live scores with params:", {
+ sportId,
+ leagueId,
+ timezone,
+ });
try {
const params: { sport_id: number; league_id?: number; timezone?: string } =
{
diff --git a/types/api.ts b/types/api.ts
index 1ffb97d..593dad2 100644
--- a/types/api.ts
+++ b/types/api.ts
@@ -58,6 +58,7 @@ export interface LiveScoreMatch {
away_team_logo: string;
event_final_result: string;
event_halftime_result: string;
+ event_game_result?: string;
event_status: string;
event_live: string;
league_key: number;
@@ -103,20 +104,44 @@ export interface LiveScoreMatch {
substitutes?: {
time: string;
home_scorer:
- | { in: string; out: string; in_id: number; out_id: number }
- | any[];
+ | { in: string; out: string; in_id: number; out_id: number }
+ | any[];
away_scorer:
- | { in: string; out: string; in_id: number; out_id: number }
- | any[];
+ | { in: string; out: string; in_id: number; out_id: number }
+ | any[];
info: string;
info_time: string;
score: string;
}[];
lineups?: unknown;
- statistics?: {
- type: string;
- home: string;
- away: string;
+ statistics?:
+ | {
+ type: string;
+ home: string;
+ away: string;
+ }[]
+ | {
+ name: string;
+ period: string;
+ player_key: number;
+ value: string;
+ type?: string;
+ total?: number | null;
+ won?: number | null;
+ }[];
+ pointbypoint?: {
+ number_game: string;
+ player_served: string;
+ score: string;
+ serve_winner: string;
+ set_number: string;
+ points: {
+ break_point: null | string;
+ match_point: null | string;
+ number_point: string;
+ score: string;
+ set_point: null | string;
+ }[];
}[];
}