From c63753e6318af4af3f7fd4310b1a159e25f638fb Mon Sep 17 00:00:00 2001 From: yuchenglong Date: Thu, 22 Jan 2026 18:48:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BD=91=E7=90=83=E8=AF=A6?= =?UTF-8?q?=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/live-detail/[id].tsx | 28 +- components/live-detail/live-score-header.tsx | 62 ++- components/live-detail/stats-card.tsx | 9 +- components/live-detail/tennis-power-graph.tsx | 332 ++++++++++++++ components/live-detail/tennis-scoreboard.tsx | 244 +++++++++++ components/live-detail/tennis-stats-card.tsx | 408 ++++++++++++++++++ components/match-card.tsx | 9 +- i18n/locales/en.json | 12 + i18n/locales/hi.json | 1 + i18n/locales/id.json | 1 + i18n/locales/ms.json | 1 + i18n/locales/th.json | 1 + i18n/locales/vi.json | 1 + i18n/locales/zh.json | 12 + lib/api.ts | 12 +- types/api.ts | 41 +- 16 files changed, 1142 insertions(+), 32 deletions(-) create mode 100644 components/live-detail/tennis-power-graph.tsx create mode 100644 components/live-detail/tennis-scoreboard.tsx create mode 100644 components/live-detail/tennis-stats-card.tsx 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 */} + + + + {/* Outer Track */} + + + {/* P1 Progress (Outer) */} + + + {/* Inner Track */} + + + {/* P2 Progress (Inner) */} + + + + + + {/* 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; + }[]; }[]; }