Files
physical-expo/app/live-detail/[id].tsx
2026-01-23 14:02:43 +08:00

308 lines
9.6 KiB
TypeScript

import { EventsTimeline } from "@/components/live-detail/events-timeline";
import { LiveLeagueInfo } from "@/components/live-detail/live-league-info";
import { LiveMatchTabs } from "@/components/live-detail/live-match-tabs";
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 { BasketballOverallStats } from "@/components/match-detail/basketball/basketball-overall-stats";
import { BasketballScoreTable } from "@/components/match-detail/basketball/basketball-score-table";
import { BasketballStats } from "@/components/match-detail/basketball/basketball-stats";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { fetchLiveScore } from "@/lib/api";
import { LiveScoreMatch, MatchDetailData } from "@/types/api";
import { useLocalSearchParams } from "expo-router";
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function LiveDetailScreen() {
const { id, league_id, sport_id } = useLocalSearchParams<{
id: string;
league_id: string;
sport_id: string;
}>();
const { theme } = useTheme();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const isDark = theme === "dark";
const [loading, setLoading] = useState(true);
const [match, setMatch] = useState<LiveScoreMatch | null>(null);
const [activeTab, setActiveTab] = useState("detail");
const convertToMatchDetailData = useMemo((): MatchDetailData | null => {
if (!match) return null;
return {
events: [],
match: {
ID: 0,
CreatedAt: "",
UpdatedAt: "",
DeletedAt: null,
eventKey: match.event_key?.toString() || "",
eventDate: match.event_date,
eventTime: match.event_time,
eventHomeTeam: match.event_home_team,
homeTeamKey: match.home_team_key?.toString() || "",
homeTeamLogo: match.home_team_logo || "",
eventAwayTeam: match.event_away_team,
awayTeamKey: match.away_team_key?.toString() || "",
awayTeamLogo: match.away_team_logo || "",
eventHalftimeResult: match.event_halftime_result || "",
eventFinalResult: match.event_final_result,
eventFtResult: "",
eventPenaltyResult: "",
eventStatus: match.event_status,
countryName: match.country_name,
leagueName: match.league_name,
leagueKey: match.league_key?.toString() || "",
leagueRound: match.league_round,
leagueSeason: match.league_season,
eventLive: match.event_live,
eventStadium: "",
eventReferee: "",
eventCountryKey: match.event_country_key?.toString() || "",
leagueLogo: match.league_logo || "",
countryLogo: match.country_logo || "",
eventHomeFormation: "",
eventAwayFormation: "",
fkStageKey: "",
stageName: "",
leagueGroup: "",
sportId: parseInt(sport_id || "1"),
eventQuarter: match.event_quarter || "",
eventSet: "",
eventType: "",
eventToss: "",
eventManOfMatch: "",
scores: Array.isArray(match.scores) && match.scores.length > 0 && 'type' in match.scores[0]
? match.scores as { type: string; home: string; away: string; }[]
: undefined,
stats: Array.isArray(match.statistics) && match.statistics.length > 0 && 'type' in match.statistics[0]
? match.statistics as { type: string; home: string; away: string; }[]
: undefined,
players: match.player_statistics
? {
home_team: (match.player_statistics.home_team || []).map((p) => ({
...p,
player_oncourt: p.player_oncourt || undefined,
})),
away_team: (match.player_statistics.away_team || []).map((p) => ({
...p,
player_oncourt: p.player_oncourt || undefined,
})),
}
: undefined,
},
};
}, [match, sport_id]);
useEffect(() => {
loadLiveDetail();
const timer = setInterval(() => {
refreshLiveDetail();
}, 15000);
return () => clearInterval(timer);
}, [id, league_id]);
const loadLiveDetail = async () => {
try {
setLoading(true);
await refreshLiveDetail();
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const refreshLiveDetail = async () => {
try {
const sportId = parseInt(sport_id || "1");
const leagueId = parseInt(league_id || "0");
const liveData = await fetchLiveScore(sportId, leagueId);
if (liveData && Array.isArray(liveData)) {
const found = liveData.find((m) => m.event_key.toString() === id);
// console.log("found", JSON.stringify(found, null, 2));
if (found) {
setMatch(found);
}
}
} catch (err) {
console.error("Refresh live detail error:", err);
}
};
if (loading) {
return (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" color={Colors[theme].tint} />
</ThemedView>
);
}
if (!match) {
return (
<ThemedView style={styles.center}>
<ThemedText>{t("detail.not_found")}</ThemedText>
<TouchableOpacity style={styles.retryButton} onPress={loadLiveDetail}>
<ThemedText style={styles.retryText}>{t("detail.retry")}</ThemedText>
</TouchableOpacity>
</ThemedView>
);
}
const renderTabContent = () => {
if (!match) return null;
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 || ""));
if (numericSportId === 2 && convertToMatchDetailData) {
switch (activeTab) {
case "stats":
return (
<BasketballStats data={convertToMatchDetailData} isDark={isDark} />
);
case "overall":
return (
<BasketballOverallStats
data={convertToMatchDetailData}
isDark={isDark}
/>
);
case "odds":
return (
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
);
case "detail":
return (
<>
<BasketballScoreTable
data={convertToMatchDetailData}
isDark={isDark}
/>
</>
);
default:
return (
<View style={styles.center}>
<ThemedText style={{ opacity: 0.5 }}>
{t("detail.empty_stats")}
</ThemedText>
</View>
);
}
}
switch (activeTab) {
case "stats":
return !isTennis ? (
<StatsCard match={match} isDark={isDark} />
) : (
<View style={styles.center}>
<ThemedText style={{ opacity: 0.5 }}>
{t("detail.empty_stats")}
</ThemedText>
</View>
);
case "odds":
return (
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
);
case "detail":
if (isTennis) {
return (
<>
<TennisScoreboard match={match} isDark={isDark} />
<TennisStatsCard match={match} isDark={isDark} />
<TennisPowerGraph match={match} isDark={isDark} />
</>
);
}
return (
<>
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
<StatsCard match={match} isDark={isDark} />
<EventsTimeline match={match} isDark={isDark} />
<OtherInfoCard match={match} isDark={isDark} />
</>
);
default:
return (
<View style={styles.center}>
<ThemedText style={{ opacity: 0.5 }}>
{t("detail.empty_stats")}
</ThemedText>
</View>
);
}
};
return (
<ThemedView style={styles.container}>
<ScrollView
bounces={false}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 20 }}
>
<LiveScoreHeader match={match} topInset={insets.top} />
<LiveLeagueInfo match={match} />
<LiveMatchTabs
activeTab={activeTab}
onTabChange={setActiveTab}
isDark={isDark}
sportId={parseInt(sport_id || "1")}
/>
{renderTabContent()}
</ScrollView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
center: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
minHeight: 200,
},
retryButton: {
marginTop: 20,
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: "#007AFF",
borderRadius: 8,
},
retryText: {
color: "#FFF",
fontWeight: "600",
},
});