335 lines
9.8 KiB
TypeScript
335 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}>
|
|
{/* Spacer top */}
|
|
<View style={{ flex: 1 }} />
|
|
<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>
|
|
{/* Spacer bottom */}
|
|
<View style={{ flex: 1 }} />
|
|
</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,
|
|
},
|
|
});
|