345 lines
9.1 KiB
TypeScript
345 lines
9.1 KiB
TypeScript
import { MatchCard } from "@/components/match-card";
|
|
import { ThemedText } from "@/components/themed-text";
|
|
import { ThemedView } from "@/components/themed-view";
|
|
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 { storage } from "@/lib/storage";
|
|
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 token = await storage.getAccessToken();
|
|
if (!token) {
|
|
setFavorites([]);
|
|
setHasMore(false);
|
|
return;
|
|
}
|
|
|
|
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}>
|
|
{/* 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>
|
|
);
|
|
}
|
|
|
|
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",
|
|
},
|
|
});
|