diff --git a/app/(tabs)/upcoming.tsx b/app/(tabs)/upcoming.tsx index c92bd5e..6df174a 100644 --- a/app/(tabs)/upcoming.tsx +++ b/app/(tabs)/upcoming.tsx @@ -1,11 +1,285 @@ +import { HomeHeader } from "@/components/home-header"; +import { LeagueModal } from "@/components/league-modal"; +import { SelectionModal } from "@/components/selection-modal"; import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; -import { StyleSheet } from "react-native"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { UpcomingMatchCard } from "@/components/upcoming-match-card"; +import { Colors } from "@/constants/theme"; +import { useTheme } from "@/context/ThemeContext"; +import { fetchLeagues, fetchSports, fetchUpcomingMatches } from "@/lib/api"; +import { League, Sport, UpcomingMatch } from "@/types/api"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ActivityIndicator, + FlatList, + StyleSheet, + TouchableOpacity, + View, +} from "react-native"; + +export default function HomeScreen() { + const { theme } = useTheme(); + const { t } = useTranslation(); + const isDark = theme === "dark"; + const iconColor = isDark ? Colors.dark.icon : Colors.light.icon; + const filterBg = isDark ? "#2C2C2E" : "#F2F2F7"; + + const [sports, setSports] = useState([]); + const [leagues, setLeagues] = useState([]); + const [matches, setMatches] = useState([]); + const [loading, setLoading] = useState(true); + const [loadingLeagues, setLoadingLeagues] = useState(false); + + // Selection States + // 默认足球 + const [selectedSportId, setSelectedSportId] = useState(1); + const [selectedLeagueKey, setSelectedLeagueKey] = useState(""); // 默认空字符串 + + // Modal Visibilities + const [showSportModal, setShowSportModal] = useState(false); + const [showLeagueModal, setShowLeagueModal] = useState(false); + + // Load Sports and Leagues + useEffect(() => { + loadSports(); + loadLeagues(); + }, []); + + // Load Matches when sport or league changes + useEffect(() => { + if (selectedSportId !== null) { + loadMatches(); + } + }, [selectedSportId, selectedLeagueKey]); + + // Load Leagues when sport changes + useEffect(() => { + if (selectedSportId !== null) { + loadLeagues(); + } + }, [selectedSportId]); + + const loadSports = async () => { + try { + const apiList = await fetchSports(); + // 创建8个运动的完整列表 + const defaultSports: Sport[] = [ + { id: 1, name: "football", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 2, name: "basketball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 3, name: "tennis", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 4, name: "cricket", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 5, name: "baseball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 6, name: "badminton", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 7, name: "snooker", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 8, name: "volleyball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + ]; + + // 合并API返回的运动和默认列表 + const sportsMap = new Map(); + apiList.forEach((sport) => { + sportsMap.set(sport.id, sport); + }); + + // 补充默认运动到8个 + defaultSports.forEach((sport) => { + if (!sportsMap.has(sport.id)) { + sportsMap.set(sport.id, sport); + } + }); + + const allSports = Array.from(sportsMap.values()) + .sort((a, b) => a.id - b.id) + .slice(0, 8); + + setSports(allSports); + } catch (e) { + console.error(e); + // API失败时使用默认8个运动 + const defaultSports: Sport[] = [ + { id: 1, name: "football", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 2, name: "basketball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 3, name: "tennis", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 4, name: "cricket", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 5, name: "baseball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 6, name: "badminton", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 7, name: "snooker", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + { id: 8, name: "volleyball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" }, + ]; + setSports(defaultSports); + setSelectedSportId(1); + } + }; + + // 加载联赛 + const loadLeagues = async () => { + try { + if (selectedSportId !== null) { + setLoadingLeagues(true); + const list = await fetchLeagues(selectedSportId, ""); + setLeagues(list); + } + } catch (e) { + console.error(e); + } finally { + setLoadingLeagues(false); + } + }; + + const loadMatches = async () => { + if (selectedSportId === null) return; + + setLoading(true); + try { + // 使用 fetchUpcomingMatches,默认 leagueKey 为空字符串 + const list = await fetchUpcomingMatches(selectedSportId, selectedLeagueKey || ""); + setMatches(list); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const currentSport = sports.find((s) => s.id === selectedSportId); + + // 获取当前运动的国际化名称 + const getSportName = (sport: Sport | undefined): string => { + if (!sport) return t("home.select_sport"); + const sportKeyMap: { [key: number]: string } = { + 1: "football", + 2: "basketball", + 3: "tennis", + 4: "cricket", + 5: "baseball", + 6: "badminton", + 7: "snooker", + 8: "volleyball", + }; + const sportKey = sportKeyMap[sport.id] || sport.name.toLowerCase(); + return t(`sports.${sportKey}`, { defaultValue: sport.name }); + }; + + const handleLeagueSelect = (leagueKey: string) => { + setSelectedLeagueKey(leagueKey); + console.log("Selected league:", leagueKey); + }; + + const renderHeader = () => ( + + {/* League Filter (Fixed) */} + + + + {t("home.league")} + + + + {/* Sport Selector */} + setShowSportModal(true)} + > + + + {getSportName(currentSport)} + + + + {/* League Selector */} + { + // 立即显示弹窗 + setShowLeagueModal(true); + // 如果联赛列表为空,立即设置loading状态并加载 + if (selectedSportId !== null) { + if (leagues.length === 0) { + setLoadingLeagues(true); + } + loadLeagues(); + } + }} + > + + + {selectedLeagueKey + ? leagues.find(l => l.key === selectedLeagueKey)?.name || t("home.select_league") + : t("home.all_leagues")} + + + + ); -export default function UpcomingScreen() { return ( - Upcoming Events + + + {renderHeader()} + + {loading ? ( + + + {t("home.loading")} + + ) : ( + item.id.toString()} + renderItem={({ item }) => } + contentContainerStyle={styles.listContent} + ListEmptyComponent={ + + {t("home.no_matches")} + + } + /> + )} + + {/* Modals */} + setShowSportModal(false)} + title={t("home.select_sport")} + options={sports.map((s) => { + const sportKeyMap: { [key: number]: string } = { + 1: "football", + 2: "basketball", + 3: "tennis", + 4: "cricket", + 5: "baseball", + 6: "badminton", + 7: "snooker", + 8: "volleyball", + }; + const sportKey = sportKeyMap[s.id] || s.name.toLowerCase(); + return { + id: s.id, + label: s.name, // 保留原始名称用于图标识别 + value: s.id, + icon: s.icon, + }; + })} + selectedValue={selectedSportId} + onSelect={setSelectedSportId} + /> + + setShowLeagueModal(false)} + leagues={leagues} + selectedLeagueKey={selectedLeagueKey} + loading={loadingLeagues} + onSelect={handleLeagueSelect} + /> ); } @@ -13,7 +287,53 @@ export default function UpcomingScreen() { const styles = StyleSheet.create({ container: { flex: 1, - alignItems: "center", + }, + center: { + flex: 1, justifyContent: "center", + alignItems: "center", + paddingTop: 50, + }, + filterContainer: { + flexDirection: "row", + paddingHorizontal: 16, + paddingVertical: 12, + gap: 12, + }, + filterBtn: { + flex: 1, + height: 44, // Increased from 36 + flexDirection: "column", // Stacked logic for Date, or Row for others + justifyContent: "center", + alignItems: "center", + borderRadius: 8, // Rounded corners + // iOS shadow + shadowColor: "#000", + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + // Android elevation + elevation: 2, + }, + mainFilterBtn: { + flex: 2, // Wider for sport + flexDirection: "row", + gap: 8, + }, + filterText: { + fontSize: 14, + fontWeight: "500", + }, + dateDayText: { + fontSize: 16, + fontWeight: "bold", + }, + dateMonthText: { + fontSize: 10, + opacity: 0.6, + }, + listContent: { + padding: 16, + paddingTop: 8, }, }); diff --git a/components/upcoming-match-card.tsx b/components/upcoming-match-card.tsx new file mode 100644 index 0000000..6ca29ba --- /dev/null +++ b/components/upcoming-match-card.tsx @@ -0,0 +1,194 @@ +import { ThemedText } from "@/components/themed-text"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { Colors } from "@/constants/theme"; +import { useTheme } from "@/context/ThemeContext"; +import { UpcomingMatch } from "@/types/api"; +import { Image } from "expo-image"; +import { useRouter } from "expo-router"; +import React from "react"; +import { Pressable, StyleSheet, View } from "react-native"; + +interface UpcomingMatchCardProps { + match: UpcomingMatch; +} + +export function UpcomingMatchCard({ match }: UpcomingMatchCardProps) { + const router = useRouter(); + const { theme } = useTheme(); + const isDark = theme === "dark"; + const iconColor = isDark ? Colors.dark.icon : Colors.light.icon; + const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; + const borderColor = isDark ? "#38383A" : "#E5E5EA"; + + const handlePress = () => { + router.push(`/match-detail/${match.eventKey}`); + }; + + // 格式化日期时间 + const formatDateTime = () => { + if (match.eventDate && match.eventTime) { + return `${match.eventDate} ${match.eventTime}`; + } + return match.eventDate || match.eventTime || ""; + }; + + return ( + [ + styles.card, + { backgroundColor: cardBg, borderColor, opacity: pressed ? 0.7 : 1 }, + ]} + > + + + {match.leagueLogo && ( + + )} + + {match.leagueName} + + + {formatDateTime()} + + + + + {match.homeTeamLogo && ( + + )} + + {match.homeTeamName} + + + + + VS + + + + {match.awayTeamLogo && ( + + )} + + {match.awayTeamName} + + + + + {(match.venue || match.round) && ( + + {match.venue && ( + + + {match.venue} + + )} + {match.round && ( + + {match.round} + + )} + + )} + + ); +} + +const styles = StyleSheet.create({ + card: { + padding: 12, + marginBottom: 12, + borderRadius: 12, + borderWidth: 1, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 12, + }, + leagueBadge: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + flex: 1, + marginRight: 8, + }, + leagueLogo: { + width: 16, + height: 16, + marginRight: 4, + }, + leagueText: { + fontSize: 12, + opacity: 0.7, + flex: 1, + }, + timeText: { + fontSize: 12, + opacity: 0.5, + }, + teamsContainer: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, + team: { + flex: 1, + flexDirection: "row", + alignItems: "center", + }, + teamLogo: { + width: 24, + height: 24, + marginRight: 8, + }, + teamName: { + fontSize: 14, + flex: 1, + }, + vsContainer: { + paddingHorizontal: 12, + }, + vsText: { + fontSize: 12, + opacity: 0.5, + fontWeight: "600", + }, + metaContainer: { + flexDirection: "row", + alignItems: "center", + flexWrap: "wrap", + gap: 8, + marginTop: 4, + }, + metaItem: { + flexDirection: "row", + alignItems: "center", + gap: 4, + }, + metaText: { + fontSize: 11, + opacity: 0.5, + }, +}); diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 290f7a0..b8e4e75 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -23,6 +23,7 @@ "league": "League", "select_sport": "Sport", "select_league": "Select League", + "all_leagues": "All Leagues", "loading": "Loading...", "no_matches": "No matches found.", "no_leagues": "No leagues available" diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index 4dd60ac..528e0c5 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -23,6 +23,7 @@ "league": "联赛", "select_sport": "运动", "select_league": "选择联赛", + "all_leagues": "全部联赛", "loading": "加载中...", "no_matches": "暂无比赛", "no_leagues": "暂无联赛"