实现收藏页面
This commit is contained in:
@@ -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<FilterType>("match");
|
||||
const [favorites, setFavorites] = useState<FavoriteItem[]>([]);
|
||||
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 (
|
||||
<MatchCard
|
||||
match={mappedMatch}
|
||||
onFavoriteToggle={() => 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 (
|
||||
<ThemedView
|
||||
style={[
|
||||
styles.genericItem,
|
||||
{ borderColor: isDark ? "#38383A" : "#E5E5EA" },
|
||||
]}
|
||||
>
|
||||
<View style={styles.itemContent}>
|
||||
<Image
|
||||
source={{ uri: logo || "https://via.placeholder.com/40" }}
|
||||
style={{ width: 40, height: 40, borderRadius: 20, marginRight: 12 }}
|
||||
contentFit="contain"
|
||||
/>
|
||||
<View>
|
||||
<ThemedText>{name}</ThemedText>
|
||||
{desc ? (
|
||||
<ThemedText style={{ fontSize: 12, color: "#8E8E93" }}>
|
||||
{desc}
|
||||
</ThemedText>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => handleUnfavorite(activeTab, item.typeId)}
|
||||
>
|
||||
<IconSymbol name="star" size={24} color="#FFD700" />
|
||||
</TouchableOpacity>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">Favorites</ThemedText>
|
||||
{/* Header */}
|
||||
<View style={[styles.header, { paddingTop: insets.top + 10 }]}>
|
||||
<ThemedText type="title" style={{ fontSize: 28 }}>
|
||||
{t("tabs.fav")}
|
||||
</ThemedText>
|
||||
<TouchableOpacity>
|
||||
<IconSymbol name="create-outline" size={24} color={iconColor} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Tabs */}
|
||||
<View>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.tabContainer}
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={tab.key}
|
||||
style={[
|
||||
styles.tabButton,
|
||||
isActive && {
|
||||
backgroundColor: isDark ? "#3A3A3C" : "#E5E5EA",
|
||||
borderColor: isDark ? "#3A3A3C" : "#E5E5EA",
|
||||
},
|
||||
!isActive && {
|
||||
borderColor: isDark ? "#38383A" : "#E5E5EA",
|
||||
},
|
||||
]}
|
||||
onPress={() => setActiveTab(tab.key)}
|
||||
>
|
||||
<IconSymbol
|
||||
name={tab.icon}
|
||||
size={16}
|
||||
color={isActive ? textColor : "#8E8E93"}
|
||||
/>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.tabText,
|
||||
{ color: isActive ? textColor : "#8E8E93" },
|
||||
]}
|
||||
>
|
||||
{tab.label}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<FlatList
|
||||
data={favorites}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item }) => {
|
||||
if (activeTab === "match") return renderMatchItem(item);
|
||||
return renderGenericItem(item);
|
||||
}}
|
||||
contentContainerStyle={styles.list}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
onEndReached={loadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListFooterComponent={
|
||||
loading && favorites.length > 0 ? (
|
||||
<ActivityIndicator style={{ padding: 10 }} />
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!loading ? (
|
||||
<View style={styles.empty}>
|
||||
<ThemedText style={{ color: "#8E8E93" }}>
|
||||
{t("favorites.no_data")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user