Files
physical-expo/components/live-detail/tennis-power-graph.tsx
2026-01-22 18:48:39 +08:00

333 lines
9.8 KiB
TypeScript

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 (
<ThemedView
style={[
styles.container,
{
backgroundColor: isDark ? "#1E1E20" : "#FFF",
borderRadius: 12,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
]}
>
<ThemedText style={[styles.title, { color: isDark ? "#FFF" : "#000" }]}>
{t("detail.power_graph")}
</ThemedText>
<View style={styles.contentRow}>
{/* Sidebar - Avatars */}
<View style={styles.sidebar}>
<View style={{ flex: 1 }} /> {/* Spacer top */}
<View style={styles.avatarContainer}>
{p1Logo ? (
<Image source={{ uri: p1Logo }} style={styles.avatar} />
) : (
<View style={[styles.avatar, { backgroundColor: "#ccc" }]} />
)}
</View>
<View style={{ height: 10 }} />{" "}
{/* Gap corresponding to center line if needed, but styling is easier with even spacing */}
<View style={styles.avatarContainer}>
{p2Logo ? (
<Image source={{ uri: p2Logo }} style={styles.avatar} />
) : (
<View style={[styles.avatar, { backgroundColor: "#ccc" }]} />
)}
</View>
<View style={{ flex: 1 }} /> {/* Spacer bottom */}
</View>
{/* Scrollable Graph */}
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.graphScroll}
>
{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 (
<View key={i} style={styles.gameBlock}>
{/* Top Score Label if needed */}
<View style={styles.gameTopLabel} />
{/* Bars Area */}
<View style={styles.barsArea}>
{/* Backgrounds */}
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "50%",
backgroundColor: topBg,
}}
/>
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "50%",
backgroundColor: botBg,
}}
/>
{/* Center Line */}
<View
style={[
styles.centerLine,
{ backgroundColor: "#FFF", height: 2, zIndex: 1 },
]}
/>
{game.bars.map((bar, bi) => (
<View key={bi} style={[styles.barColumn, { zIndex: 2 }]}>
{/* P1 Bar (Top) */}
<View
style={[
styles.barSegment,
{
height: 24,
justifyContent: "flex-end",
},
]}
>
{bar.winner === 1 && (
<View
style={[
styles.activeBar,
{
backgroundColor: "#2196F3",
height:
bi === game.bars.length - 1 ? "100%" : "60%",
},
]}
/>
)}
</View>
{/* P2 Bar (Bottom) */}
<View
style={[
styles.barSegment,
{
height: 24,
justifyContent: "flex-start",
},
]}
>
{bar.winner === 2 && (
<View
style={[
styles.activeBar,
{
backgroundColor: "#FFC107",
height:
bi === game.bars.length - 1 ? "100%" : "60%",
},
]}
/>
)}
</View>
</View>
))}
</View>
{/* Bottom Game Number */}
<View style={styles.gameBottomLabel}>
<ThemedText style={styles.gameNumberText}>
{game.gameIndex}
</ThemedText>
</View>
</View>
);
})}
</ScrollView>
</View>
</ThemedView>
);
}
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,
},
});