实现直播详情页
This commit is contained in:
534
components/live-detail/events-timeline.tsx
Normal file
534
components/live-detail/events-timeline.tsx
Normal file
@@ -0,0 +1,534 @@
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { IconSymbol } from "@/components/ui/icon-symbol";
|
||||
import { LiveScoreMatch } from "@/types/api";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, Switch, View } from "react-native";
|
||||
|
||||
interface EventItem {
|
||||
time: string;
|
||||
type: "goal" | "card" | "sub" | "status";
|
||||
side: "home" | "away" | "center";
|
||||
player?: string;
|
||||
playerIn?: string;
|
||||
playerOut?: string;
|
||||
detail?: string;
|
||||
score?: string;
|
||||
isPenalty?: boolean;
|
||||
}
|
||||
|
||||
interface EventsTimelineProps {
|
||||
match: LiveScoreMatch;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
export function EventsTimeline({ match, isDark }: EventsTimelineProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showSub, setShowSub] = useState(true);
|
||||
const [showCard, setShowCard] = useState(true);
|
||||
|
||||
// Parse and merge events
|
||||
const events: EventItem[] = [];
|
||||
|
||||
// 1. Goals
|
||||
match.goalscorers?.forEach((g) => {
|
||||
const isHome = !!g.home_scorer;
|
||||
events.push({
|
||||
time: g.time,
|
||||
type: "goal",
|
||||
side: isHome ? "home" : "away",
|
||||
player: isHome ? g.home_scorer : g.away_scorer,
|
||||
detail: isHome ? g.home_assist : g.away_assist,
|
||||
score: g.score,
|
||||
isPenalty: g.info?.toLowerCase().includes("penalty"),
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Cards
|
||||
match.cards?.forEach((c) => {
|
||||
const isHome = !!c.home_fault;
|
||||
events.push({
|
||||
time: c.time,
|
||||
type: "card",
|
||||
side: isHome ? "home" : "away",
|
||||
player: isHome ? c.home_fault : c.away_fault,
|
||||
detail: c.card, // "yellow card" or "red card"
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Substitutes
|
||||
match.substitutes?.forEach((s) => {
|
||||
const isHome = Array.isArray(s.home_scorer) ? false : !!s.home_scorer?.in;
|
||||
const isAway = Array.isArray(s.away_scorer) ? false : !!s.away_scorer?.in;
|
||||
|
||||
if (isHome) {
|
||||
const h = s.home_scorer as any;
|
||||
events.push({
|
||||
time: s.time,
|
||||
type: "sub",
|
||||
side: "home",
|
||||
playerIn: h.in,
|
||||
playerOut: h.out,
|
||||
});
|
||||
}
|
||||
if (isAway) {
|
||||
const a = s.away_scorer as any;
|
||||
events.push({
|
||||
time: s.time,
|
||||
type: "sub",
|
||||
side: "away",
|
||||
playerIn: a.in,
|
||||
playerOut: a.out,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Status items
|
||||
events.push({
|
||||
time: "0",
|
||||
type: "status",
|
||||
side: "center",
|
||||
detail: t("detail.events.start"),
|
||||
});
|
||||
|
||||
if (match.event_halftime_result) {
|
||||
events.push({
|
||||
time: "45",
|
||||
type: "status",
|
||||
side: "center",
|
||||
detail: `${t("detail.events.ht")} ${match.event_halftime_result}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by time (Descending - Newer at top)
|
||||
const sortedEvents = events.sort((a, b) => {
|
||||
const timeA = parseInt(a.time.replace("'", "")) || 0;
|
||||
const timeB = parseInt(b.time.replace("'", "")) || 0;
|
||||
return timeB - timeA;
|
||||
});
|
||||
|
||||
const filteredEvents = sortedEvents.filter((ev) => {
|
||||
if (ev.type === "sub") return showSub;
|
||||
if (ev.type === "card") return showCard;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={[styles.container, isDark && styles.darkContainer]}>
|
||||
<View style={styles.timelineWrapper}>
|
||||
<View style={[styles.centerLine, isDark && styles.darkLine]} />
|
||||
|
||||
{filteredEvents.map((event, index) => (
|
||||
<View key={index} style={styles.eventRow}>
|
||||
{/* Home Side */}
|
||||
<View style={styles.sideContainer}>
|
||||
{event.side === "home" && (
|
||||
<View style={[styles.eventContent, styles.homeContent]}>
|
||||
{event.type === "goal" && (
|
||||
<View style={styles.goalBox}>
|
||||
<ThemedText style={styles.eventLabel}>
|
||||
{event.isPenalty
|
||||
? t("detail.events.penalty_goal")
|
||||
: t("detail.events.goal")}
|
||||
</ThemedText>
|
||||
<View
|
||||
style={[
|
||||
styles.playerIconPlaceholder,
|
||||
isDark && styles.darkItem,
|
||||
isDark && { borderColor: "#333" },
|
||||
]}
|
||||
>
|
||||
<IconSymbol
|
||||
name="person"
|
||||
size={16}
|
||||
color={isDark ? "#666" : "#BBB"}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[styles.scorePill, isDark && styles.darkItem]}
|
||||
>
|
||||
<IconSymbol
|
||||
name="football"
|
||||
size={12}
|
||||
color={isDark ? "#FFF" : "#000"}
|
||||
/>
|
||||
<ThemedText
|
||||
style={[styles.scoreLabel, isDark && styles.darkText]}
|
||||
>
|
||||
{event.score}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{event.type === "card" && (
|
||||
<View
|
||||
style={[
|
||||
styles.cardPill,
|
||||
isDark && styles.darkItem,
|
||||
{ borderColor: isDark ? "#333" : "#EEE" },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.miniCard,
|
||||
{
|
||||
backgroundColor: event.detail?.includes("yellow")
|
||||
? "#FFD700"
|
||||
: "#FF3B30",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{event.type === "sub" && (
|
||||
<View style={styles.subBox}>
|
||||
<ThemedText
|
||||
style={[styles.subInText, isDark && styles.darkText]}
|
||||
>
|
||||
{event.playerIn}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.subOutText}>
|
||||
{event.playerOut}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Time Point */}
|
||||
<View style={styles.timePointContainer}>
|
||||
{event.type !== "status" ? (
|
||||
<View style={[styles.timeCircle, isDark && styles.darkItem]}>
|
||||
<ThemedText
|
||||
style={[styles.timeText, isDark && styles.darkText]}
|
||||
>
|
||||
{`${event.time}'`}
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
styles.statusPill,
|
||||
isDark && styles.darkItem,
|
||||
isDark && { borderColor: "#333" },
|
||||
]}
|
||||
>
|
||||
<ThemedText
|
||||
style={[styles.statusText, isDark && styles.darkText]}
|
||||
>
|
||||
{event.detail}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Away Side */}
|
||||
<View style={styles.sideContainer}>
|
||||
{event.side === "away" && (
|
||||
<View style={[styles.eventContent, styles.awayContent]}>
|
||||
{event.type === "goal" && (
|
||||
<View style={styles.goalBox}>
|
||||
<View
|
||||
style={[styles.scorePill, isDark && styles.darkItem]}
|
||||
>
|
||||
<ThemedText
|
||||
style={[styles.scoreLabel, isDark && styles.darkText]}
|
||||
>
|
||||
{event.score}
|
||||
</ThemedText>
|
||||
<IconSymbol
|
||||
name="football"
|
||||
size={12}
|
||||
color={isDark ? "#FFF" : "#000"}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
styles.playerIconPlaceholder,
|
||||
isDark && styles.darkItem,
|
||||
isDark && { borderColor: "#333" },
|
||||
]}
|
||||
>
|
||||
<IconSymbol
|
||||
name="person"
|
||||
size={16}
|
||||
color={isDark ? "#666" : "#BBB"}
|
||||
/>
|
||||
</View>
|
||||
<ThemedText style={styles.eventLabel}>
|
||||
{event.isPenalty
|
||||
? t("detail.events.penalty_goal")
|
||||
: t("detail.events.goal")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
{event.type === "card" && (
|
||||
<View
|
||||
style={[
|
||||
styles.cardPill,
|
||||
isDark && styles.darkItem,
|
||||
{ borderColor: isDark ? "#333" : "#EEE" },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.miniCard,
|
||||
{
|
||||
backgroundColor: event.detail?.includes("yellow")
|
||||
? "#FFD700"
|
||||
: "#FF3B30",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{event.type === "sub" && (
|
||||
<View style={styles.subBox}>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.subInText,
|
||||
isDark && styles.darkText,
|
||||
{ textAlign: "left" },
|
||||
]}
|
||||
>
|
||||
{event.playerIn}
|
||||
</ThemedText>
|
||||
<ThemedText
|
||||
style={[styles.subOutText, { textAlign: "left" }]}
|
||||
>
|
||||
{event.playerOut}
|
||||
</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Footer Controls */}
|
||||
<View style={[styles.footer, isDark && { borderTopColor: "#333" }]}>
|
||||
<ThemedText style={styles.footerLabel}>
|
||||
{t("detail.events.toggle_visibility")}
|
||||
</ThemedText>
|
||||
<View style={styles.footerSwitches}>
|
||||
<View style={styles.switchItem}>
|
||||
<IconSymbol name="swap-vertical" size={16} color="#4CAF50" />
|
||||
<Switch
|
||||
value={showSub}
|
||||
onValueChange={setShowSub}
|
||||
trackColor={{ false: "#DDD", true: "#3b5998" }}
|
||||
thumbColor="#FFF"
|
||||
style={styles.miniSwitch}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.switchItem}>
|
||||
<View
|
||||
style={[
|
||||
styles.miniCard,
|
||||
{ backgroundColor: "#FFD700", width: 8, height: 12 },
|
||||
]}
|
||||
/>
|
||||
<Switch
|
||||
value={showCard}
|
||||
onValueChange={setShowCard}
|
||||
trackColor={{ false: "#DDD", true: "#3b5998" }}
|
||||
thumbColor="#FFF"
|
||||
style={styles.miniSwitch}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 16,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#FFF",
|
||||
padding: 16,
|
||||
paddingBottom: 30,
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
elevation: 2,
|
||||
},
|
||||
darkContainer: {
|
||||
backgroundColor: "#1E1E20",
|
||||
shadowOpacity: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
darkItem: {
|
||||
backgroundColor: "#2C2C2E",
|
||||
borderColor: "#38383a",
|
||||
},
|
||||
darkText: {
|
||||
color: "#FFF",
|
||||
},
|
||||
timelineWrapper: {
|
||||
position: "relative",
|
||||
alignItems: "center",
|
||||
},
|
||||
centerLine: {
|
||||
position: "absolute",
|
||||
width: 1,
|
||||
height: "100%",
|
||||
backgroundColor: "#F0F0F0",
|
||||
top: 0,
|
||||
},
|
||||
darkLine: {
|
||||
backgroundColor: "#333",
|
||||
},
|
||||
eventRow: {
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
marginVertical: 12,
|
||||
},
|
||||
sideContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
timePointContainer: {
|
||||
width: 60,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
timeCircle: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
backgroundColor: "#FFF",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: "#F0F0F0",
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
color: "#333",
|
||||
},
|
||||
statusPill: {
|
||||
position: "absolute",
|
||||
backgroundColor: "#FFF",
|
||||
borderWidth: 1,
|
||||
borderColor: "#E0E0E0",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 15,
|
||||
zIndex: 2,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
eventContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
homeContent: {
|
||||
justifyContent: "flex-end",
|
||||
paddingRight: 10,
|
||||
},
|
||||
awayContent: {
|
||||
justifyContent: "flex-start",
|
||||
paddingLeft: 10,
|
||||
},
|
||||
scorePill: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#FFF",
|
||||
borderWidth: 1.5,
|
||||
borderColor: "#FFD700",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
gap: 4,
|
||||
},
|
||||
scoreLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
eventLabel: {
|
||||
fontSize: 12,
|
||||
color: "#999",
|
||||
},
|
||||
goalBox: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
playerIconPlaceholder: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: "#EEE",
|
||||
borderWidth: 1,
|
||||
borderColor: "#DDD",
|
||||
},
|
||||
cardPill: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
borderWidth: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#FFF",
|
||||
},
|
||||
miniCard: {
|
||||
width: 10,
|
||||
height: 14,
|
||||
borderRadius: 2,
|
||||
},
|
||||
subBox: {
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
subInText: {
|
||||
fontSize: 12,
|
||||
color: "#333",
|
||||
fontWeight: "500",
|
||||
},
|
||||
subOutText: {
|
||||
fontSize: 10,
|
||||
color: "#999",
|
||||
},
|
||||
penaltyIcon: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
backgroundColor: "#4B77FF",
|
||||
borderRadius: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "#F0F0F0",
|
||||
paddingTop: 16,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
footerLabel: {
|
||||
fontSize: 12,
|
||||
color: "#999",
|
||||
},
|
||||
footerSwitches: {
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
},
|
||||
switchItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
},
|
||||
miniSwitch: {
|
||||
transform: [{ scaleX: 0.7 }, { scaleY: 0.7 }],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user