From 69e7a87de6e21214bbd9bc609c06a1ad5b170a44 Mon Sep 17 00:00:00 2001 From: yuchenglong Date: Fri, 16 Jan 2026 15:18:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=94=B6=E8=97=8F=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/favorite.tsx | 323 +++++++++++++++++++++++++++++++++++++++- i18n/locales/en.json | 8 + i18n/locales/zh.json | 8 + lib/api.ts | 23 +++ types/api.ts | 19 +++ 5 files changed, 378 insertions(+), 3 deletions(-) diff --git a/app/(tabs)/favorite.tsx b/app/(tabs)/favorite.tsx index e4e9b6e..a24bed1 100644 --- a/app/(tabs)/favorite.tsx +++ b/app/(tabs)/favorite.tsx @@ -1,11 +1,283 @@ +import { MatchCard } from "@/components/match-card"; import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; -import { StyleSheet } from "react-native"; +import { IconSymbol, IconSymbolName } from "@/components/ui/icon-symbol"; +import { Colors } from "@/constants/theme"; +import { useTheme } from "@/context/ThemeContext"; +import { fetchFavorites, removeFavorite } from "@/lib/api"; +import { FavoriteItem, Match } from "@/types/api"; +import { Image } from "expo-image"; +import { useRouter } from "expo-router"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ActivityIndicator, + FlatList, + RefreshControl, + ScrollView, + StyleSheet, + TouchableOpacity, + View, +} from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +type FilterType = "match" | "team" | "player"; + +interface FilterTab { + key: FilterType; + label: string; + icon: IconSymbolName; +} export default function FavoriteScreen() { + const { theme } = useTheme(); + const { t } = useTranslation(); + const insets = useSafeAreaInsets(); + const router = useRouter(); + const isDark = theme === "dark"; + const iconColor = isDark ? Colors.dark.icon : Colors.light.icon; + const textColor = isDark ? Colors.dark.text : Colors.light.text; + + const [activeTab, setActiveTab] = useState("match"); + const [favorites, setFavorites] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + + const tabs: FilterTab[] = [ + { + key: "match", + label: t("favorites.filter_match"), + icon: "football-outline", + }, + { + key: "team", + label: t("favorites.filter_team"), + icon: "shield-outline", + }, + { + key: "player", + label: t("favorites.filter_player"), + icon: "person-outline", + }, + ]; + + const loadFavorites = async (isRefresh = false) => { + if (loading) return; + setLoading(true); + try { + const currentPage = isRefresh ? 1 : page; + const res = await fetchFavorites(activeTab, currentPage); + if (isRefresh) { + setFavorites(res.list); + setHasMore(res.list.length >= 20); // Assuming pageSize 20 + } else { + setFavorites((prev) => [...prev, ...res.list]); + setHasMore(res.list.length >= 20); + } + if (res.list.length > 0) { + setPage(currentPage + 1); + } + } catch (error) { + console.error(error); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + setPage(1); + setFavorites([]); + setHasMore(true); + loadFavorites(true); + }, [activeTab]); + + const onRefresh = () => { + setRefreshing(true); + setPage(1); + setHasMore(true); + loadFavorites(true); + }; + + const loadMore = () => { + if (!loading && hasMore) { + loadFavorites(false); + } + }; + + const handleUnfavorite = async (type: string, typeId: string) => { + try { + await removeFavorite({ type, typeId }); + setFavorites((prev) => prev.filter((item) => item.typeId !== typeId)); + } catch (error) { + console.error("Remove favorite failed", error); + } + }; + + const renderMatchItem = (item: FavoriteItem) => { + const m = item.match; + if (!m) return null; + + // Use current date as placeholder if not provided, or parse eventTime + // Assuming m contains necessary fields or we map carefully + const mappedMatch: Match = { + id: m.id + ? m.id.toString() + : item.matchId + ? item.matchId.toString() + : item.typeId, + league: m.leagueName || "", + time: m.eventTime || "", + home: m.eventHomeTeam || "", + away: m.eventAwayTeam || "", + scoreText: m.eventFinalResult || "0 - 0", + fav: true, + leagueId: m.leagueKey, + sportId: 1, // Default + isLive: false, // Provide default + }; + + return ( + handleUnfavorite("match", item.typeId)} + /> + ); + }; + + const renderGenericItem = (item: FavoriteItem) => { + let name = ""; + let logo = ""; + let desc = ""; + + if (activeTab === "team" && item.team) { + name = item.team.name; + logo = item.team.logo; + } else if (activeTab === "player" && item.player) { + name = item.player.name; + logo = item.player.photo || item.player.avatar; + desc = item.player.teamName; + } else { + name = t("favorites.unknown"); + } + + return ( + + + + + {name} + {desc ? ( + + {desc} + + ) : null} + + + handleUnfavorite(activeTab, item.typeId)} + > + + + + ); + }; + return ( - Favorites + {/* Header */} + + + {t("tabs.fav")} + + + + + + + {/* Tabs */} + + + {tabs.map((tab) => { + const isActive = activeTab === tab.key; + return ( + setActiveTab(tab.key)} + > + + + {tab.label} + + + ); + })} + + + + {/* Content */} + item.id.toString()} + renderItem={({ item }) => { + if (activeTab === "match") return renderMatchItem(item); + return renderGenericItem(item); + }} + contentContainerStyle={styles.list} + refreshControl={ + + } + onEndReached={loadMore} + onEndReachedThreshold={0.5} + ListFooterComponent={ + loading && favorites.length > 0 ? ( + + ) : null + } + ListEmptyComponent={ + !loading ? ( + + + {t("favorites.no_data")} + + + ) : null + } + /> ); } @@ -13,7 +285,52 @@ export default function FavoriteScreen() { const styles = StyleSheet.create({ container: { flex: 1, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 20, + paddingBottom: 15, + }, + tabContainer: { + flexDirection: "row", + paddingHorizontal: 16, + paddingBottom: 10, + gap: 10, + }, + tabButton: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 6, + paddingHorizontal: 12, + borderRadius: 20, + borderWidth: 1, + gap: 6, + }, + tabText: { + fontSize: 14, + fontWeight: "600", + }, + list: { + padding: 16, + paddingBottom: 100, + }, + genericItem: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + padding: 12, + marginBottom: 12, + borderRadius: 12, + borderWidth: 1, + }, + itemContent: { + flexDirection: "row", + alignItems: "center", + }, + empty: { + padding: 40, alignItems: "center", - justifyContent: "center", }, }); diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 3afc9f3..94a3ace 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -7,6 +7,14 @@ "finished": "Finished", "fav": "Favorites" }, + "favorites": { + "filter_match": "Matches", + "filter_team": "Teams", + "filter_player": "Players", + "filter_league": "Leagues", + "no_data": "No favorites found", + "unknown": "Unknown" + }, "settings": { "title": "Settings", "theme": "Theme", diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index 54289e6..acaea48 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -7,6 +7,14 @@ "finished": "已完成", "fav": "收藏" }, + "favorites": { + "filter_match": "比赛", + "filter_team": "球队", + "filter_player": "球员", + "filter_league": "联赛", + "no_data": "暂无收藏", + "unknown": "未知" + }, "settings": { "title": "设置", "theme": "主题", diff --git a/lib/api.ts b/lib/api.ts index daae61f..743e68c 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -6,6 +6,7 @@ import { AppleSignInResponse, Country, FavoriteCheckResponse, + FavoriteListResponse, FavoriteRequest, H2HData, League, @@ -483,3 +484,25 @@ export const checkFavorite = async ( throw error; } }; + +export const fetchFavorites = async ( + type: string, + page: number = 1, + pageSize: number = 20 +): Promise => { + try { + const response = await apiClient.get>( + API_ENDPOINTS.FAVORITES, + { + params: { type, page, pageSize }, + } + ); + if (response.data.code === 0) { + return response.data.data; + } + throw new Error(response.data.message); + } catch (error) { + console.error("Fetch favorites error:", error); + throw error; + } +}; diff --git a/types/api.ts b/types/api.ts index 4ed1a1f..917a2f4 100644 --- a/types/api.ts +++ b/types/api.ts @@ -490,3 +490,22 @@ export interface UserProfile { nickname: string; avatar: string; } + +export interface FavoriteItem { + id: number; + type: string; // "match" | "team" | "player" | "league" + typeId: string; + matchId?: number; + teamId?: number; + playerId?: number; + match?: any; + team?: any; + player?: any; + notify: boolean; + createdAt: string; +} + +export interface FavoriteListResponse { + list: FavoriteItem[]; + total: number; +}