diff --git a/app/_layout.tsx b/app/_layout.tsx
index db80d31..93f869b 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -68,6 +68,13 @@ function RootLayoutNav() {
headerShown: false,
}}
/>
+
diff --git a/app/search.tsx b/app/search.tsx
new file mode 100644
index 0000000..c64916e
--- /dev/null
+++ b/app/search.tsx
@@ -0,0 +1,1086 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { IconSymbol } from "@/components/ui/icon-symbol";
+import { useTheme } from "@/context/ThemeContext";
+import { BlurView } from "expo-blur";
+import { LinearGradient } from "expo-linear-gradient";
+import { Stack, useRouter } from "expo-router";
+import React, { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Animated,
+ FlatList,
+ ScrollView,
+ StyleSheet,
+ TextInput,
+ TouchableOpacity,
+ View
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+type SearchType = "team" | "player" | "league";
+
+interface SearchEntity {
+ id: string;
+ type: SearchType;
+ name: string;
+ meta: {
+ country?: string;
+ league?: string;
+ season?: string;
+ team?: string;
+ pos?: string;
+ };
+}
+
+// 模拟数据
+const ENTITIES: SearchEntity[] = [
+ { id: "t_rm", type: "team", name: "Real Madrid", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } },
+ { id: "t_fcb", type: "team", name: "FC Barcelona", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } },
+ { id: "t_mci", type: "team", name: "Manchester City", meta: { country: "England", league: "Premier League", season: "2025/26" } },
+ { id: "t_liv", type: "team", name: "Liverpool", meta: { country: "England", league: "Premier League", season: "2025/26" } },
+ { id: "t_ars", type: "team", name: "Arsenal", meta: { country: "England", league: "Premier League", season: "2025/26" } },
+ { id: "p_mbappe", type: "player", name: "Kylian Mbappé", meta: { country: "France", team: "Real Madrid", league: "LaLiga", pos: "FW", season: "2025/26" } },
+ { id: "p_haaland", type: "player", name: "Erling Haaland", meta: { country: "Norway", team: "Manchester City", league: "Premier League", pos: "FW", season: "2025/26" } },
+ { id: "p_salah", type: "player", name: "Mohamed Salah", meta: { country: "Egypt", team: "Liverpool", league: "Premier League", pos: "FW", season: "2025/26" } },
+ { id: "p_curry", type: "player", name: "Stephen Curry", meta: { country: "USA", team: "Golden State Warriors", league: "NBA", pos: "G", season: "2025/26" } },
+ { id: "l_epl", type: "league", name: "Premier League", meta: { country: "England", season: "2025/26" } },
+ { id: "l_laliga", type: "league", name: "LaLiga", meta: { country: "Spain", season: "2025/26" } },
+ { id: "l_ucl", type: "league", name: "UEFA Champions League", meta: { country: "Europe", season: "2025/26" } },
+ { id: "l_nba", type: "league", name: "NBA", meta: { country: "USA", season: "2025/26" } },
+];
+
+// 生成首字母
+function getInitials(name: string): string {
+ const s = name.trim();
+ if (!s) return "SN";
+ const clean = s.replace(/[^a-zA-Z0-9]+/g, " ").trim();
+ const parts = clean ? clean.split(/\s+/) : [s];
+ const a = (parts[0] || s).charAt(0) || "S";
+ const b = (parts[1] || "").charAt(0) || ((parts[0] || s).charAt(1) || "N");
+ return (a + b).toUpperCase();
+}
+
+// 生成颜色哈希
+function hashString(str: string): number {
+ let hash = 2166136261;
+ for (let i = 0; i < str.length; i++) {
+ hash ^= str.charCodeAt(i);
+ hash = (hash * 16777619) >>> 0;
+ }
+ return hash >>> 0;
+}
+
+// HSL 转 RGB
+function hslToRgb(h: number, s: number, l: number): string {
+ s /= 100;
+ l /= 100;
+ const c = (1 - Math.abs(2 * l - 1)) * s;
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+ const m = l - c / 2;
+ let r = 0, g = 0, b = 0;
+
+ if (0 <= h && h < 60) {
+ r = c; g = x; b = 0;
+ } else if (60 <= h && h < 120) {
+ r = x; g = c; b = 0;
+ } else if (120 <= h && h < 180) {
+ r = 0; g = c; b = x;
+ } else if (180 <= h && h < 240) {
+ r = 0; g = x; b = c;
+ } else if (240 <= h && h < 300) {
+ r = x; g = 0; b = c;
+ } else if (300 <= h && h < 360) {
+ r = c; g = 0; b = x;
+ }
+ r = Math.round((r + m) * 255);
+ g = Math.round((g + m) * 255);
+ b = Math.round((b + m) * 255);
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+// 生成渐变颜色
+function getLogoGradient(name: string): { color1: string; color2: string } {
+ const h = hashString(name);
+ const hue = h % 360;
+ const hue2 = (hue + 36 + (h % 24)) % 360;
+ return {
+ color1: hslToRgb(hue, 85, 58),
+ color2: hslToRgb(hue2, 85, 48),
+ };
+}
+
+// 获取副标题文本
+function getSubText(entity: SearchEntity): string {
+ const { meta } = entity;
+ if (entity.type === "league") {
+ return `${meta.country || ""} · ${meta.season || ""}`;
+ }
+ if (entity.type === "player") {
+ return `${meta.team || ""} · ${meta.league || ""}`;
+ }
+ return `${meta.country || ""} · ${meta.league || ""}`;
+}
+
+export default function SearchScreen() {
+ const router = useRouter();
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const insets = useSafeAreaInsets();
+ const isDark = theme === "dark";
+
+ const [searchType, setSearchType] = useState("team");
+ const [query, setQuery] = useState("");
+ const [recentSearches, setRecentSearches] = useState([
+ "Real Madrid",
+ "Premier League",
+ "Curry",
+ ]);
+ const [showDetailSheet, setShowDetailSheet] = useState(false);
+ const [selectedEntity, setSelectedEntity] = useState(null);
+
+ const sheetAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ if (showDetailSheet) {
+ Animated.spring(sheetAnim, {
+ toValue: 1,
+ useNativeDriver: true,
+ }).start();
+ } else {
+ Animated.timing(sheetAnim, {
+ toValue: 0,
+ duration: 200,
+ useNativeDriver: true,
+ }).start();
+ }
+ }, [showDetailSheet]);
+
+ const filteredEntities = ENTITIES.filter((e) => {
+ if (e.type !== searchType) return false;
+ if (!query.trim()) return true;
+ const q = query.toLowerCase();
+ return (
+ e.name.toLowerCase().includes(q) || getSubText(e).toLowerCase().includes(q)
+ );
+ });
+
+ const handleSearchTypeChange = (type: SearchType) => {
+ setSearchType(type);
+ setQuery("");
+ };
+
+ const handleRecentClick = (text: string) => {
+ setQuery(text);
+ };
+
+ const handleEntityPress = (entity: SearchEntity) => {
+ setSelectedEntity(entity);
+ setShowDetailSheet(true);
+ };
+
+ const handleClearRecent = () => {
+ setRecentSearches([]);
+ };
+
+ const handleSaveToRecent = () => {
+ if (selectedEntity) {
+ const newRecent = [
+ selectedEntity.name,
+ ...recentSearches.filter((x) => x !== selectedEntity.name),
+ ].slice(0, 10);
+ setRecentSearches(newRecent);
+ }
+ };
+
+ const handleOpenDetail = () => {
+ if (selectedEntity) {
+ handleSaveToRecent();
+ // TODO: Navigate to detail page
+ setShowDetailSheet(false);
+ }
+ };
+
+ const renderSearchBar = () => (
+
+
+
+
+ {query.length > 0 && (
+ setQuery("")}>
+
+
+ )}
+
+
+ );
+
+ const renderChips = () => (
+
+ {(["team", "player", "league"] as SearchType[]).map((type) => (
+ handleSearchTypeChange(type)}
+ >
+
+ {t(`search.type.${type}`)}
+
+
+ ))}
+
+ );
+
+ const renderRecentSearches = () => {
+ if (recentSearches.length === 0 || query.trim()) return null;
+
+ return (
+ <>
+
+
+ {t("search.recent")}
+
+
+
+ {t("search.clear")}
+
+
+
+
+ {recentSearches.map((item, index) => (
+ handleRecentClick(item)}
+ >
+
+ {item}
+
+
+ {t(`search.type.${searchType}`)}
+
+
+ ))}
+
+ >
+ );
+ };
+
+ const renderEntityItem = ({ item }: { item: SearchEntity }) => {
+ const gradient = getLogoGradient(item.name);
+ const initials = getInitials(item.name);
+
+ return (
+ handleEntityPress(item)}
+ >
+
+ {initials}
+
+
+
+ {item.name}
+
+
+ {getSubText(item)}
+
+
+
+ {t(`search.type.${item.type}`)}
+
+
+ );
+ };
+
+ const renderEmptyState = () => (
+
+
+ {t("search.no_results")}
+
+
+ {t("search.no_results_hint")}
+
+
+ );
+
+ const renderDetailSheet = () => {
+ if (!selectedEntity) return null;
+
+ const translateY = sheetAnim.interpolate({
+ inputRange: [0, 1],
+ outputRange: [600, 0],
+ });
+
+ return (
+ <>
+ setShowDetailSheet(false)}
+ >
+
+
+
+
+
+
+ {selectedEntity.name}
+
+
+ {t(`search.type.${selectedEntity.type}`)} ·{" "}
+ {getSubText(selectedEntity)}
+
+
+ setShowDetailSheet(false)}
+ >
+
+
+
+
+
+ {selectedEntity.type === "team" && (
+ <>
+
+
+ {t("search.detail.country")}
+
+
+ {selectedEntity.meta.country || "-"}
+
+
+
+
+ {t("search.detail.league")}
+
+
+ {selectedEntity.meta.league || "-"}
+
+
+
+
+ {t("search.detail.season")}
+
+
+ {selectedEntity.meta.season || "-"}
+
+
+ >
+ )}
+ {selectedEntity.type === "player" && (
+ <>
+
+
+ {t("search.detail.team")}
+
+
+ {selectedEntity.meta.team || "-"}
+
+
+
+
+ {t("search.detail.league")}
+
+
+ {selectedEntity.meta.league || "-"}
+
+
+
+
+ {t("search.detail.position")}
+
+
+ {selectedEntity.meta.pos || "-"}
+
+
+
+
+ {t("search.detail.country")}
+
+
+ {selectedEntity.meta.country || "-"}
+
+
+ >
+ )}
+ {selectedEntity.type === "league" && (
+ <>
+
+
+ {t("search.detail.country")}
+
+
+ {selectedEntity.meta.country || "-"}
+
+
+
+
+ {t("search.detail.season")}
+
+
+ {selectedEntity.meta.season || "-"}
+
+
+ >
+ )}
+
+
+
+
+
+ {t("search.detail.open")}
+
+
+
+
+ {t("search.detail.save")}
+
+
+
+
+ >
+ );
+ };
+
+ return (
+
+
+
+
+ router.back()}
+ >
+
+
+
+ {t("search.title")}
+
+ {t("search.subtitle")}
+
+
+
+
+
+
+
+
+
+ {renderSearchBar()}
+ {renderChips()}
+ {renderRecentSearches()}
+
+
+
+ {t("search.results")} · {t(`search.type.${searchType}`)}
+
+
+ {query.trim()
+ ? `${filteredEntities.length} ${t("search.found")}`
+ : t("search.tap_to_open")}
+
+
+
+
+ item.id}
+ renderItem={renderEntityItem}
+ ListEmptyComponent={() => renderEmptyState()}
+ contentContainerStyle={[
+ styles.listContent,
+ { paddingBottom: insets.bottom + 20 },
+ ]}
+ showsVerticalScrollIndicator={false}
+ />
+
+
+ {showDetailSheet && renderDetailSheet()}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ nav: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 10,
+ paddingHorizontal: 12,
+ paddingBottom: 12,
+ position: "sticky",
+ top: 0,
+ zIndex: 40,
+ },
+ iconBtn: {
+ width: 38,
+ height: 38,
+ borderRadius: 14,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ },
+ brand: {
+ flex: 1,
+ minWidth: 0,
+ gap: 2,
+ },
+ brandTitle: {
+ fontSize: 18,
+ fontWeight: "900",
+ letterSpacing: 0.2,
+ },
+ brandSub: {
+ fontSize: 12,
+ opacity: 0.42,
+ fontWeight: "800",
+ },
+ content: {
+ flex: 1,
+ },
+ contentContainer: {
+ paddingHorizontal: 12,
+ paddingTop: 8,
+ },
+ listContent: {
+ paddingHorizontal: 12,
+ },
+ searchWrap: {
+ marginBottom: 10,
+ },
+ search: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ height: 46,
+ paddingHorizontal: 12,
+ borderRadius: 18,
+ borderWidth: 1,
+ overflow: "hidden",
+ },
+ searchInput: {
+ flex: 1,
+ minWidth: 0,
+ fontSize: 15,
+ },
+ chips: {
+ marginBottom: 8,
+ },
+ chipsContent: {
+ gap: 10,
+ paddingBottom: 2,
+ },
+ chip: {
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: 999,
+ borderWidth: 1,
+ },
+ chipActive: {},
+ chipText: {
+ fontSize: 14,
+ fontWeight: "900",
+ opacity: 0.78,
+ },
+ chipTextActive: {
+ opacity: 1,
+ },
+ sectionHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginTop: 12,
+ marginBottom: 8,
+ },
+ sectionTitle: {
+ fontSize: 13,
+ opacity: 0.55,
+ fontWeight: "900",
+ },
+ sectionAction: {
+ fontSize: 12,
+ opacity: 0.42,
+ fontWeight: "800",
+ },
+ recentRow: {
+ marginBottom: 8,
+ },
+ recentRowContent: {
+ gap: 10,
+ paddingBottom: 2,
+ },
+ recentCard: {
+ minWidth: 190,
+ padding: 12,
+ borderRadius: 16,
+ borderWidth: 1,
+ },
+ recentCardTitle: {
+ fontWeight: "900",
+ },
+ recentCardSub: {
+ fontSize: 12,
+ opacity: 0.55,
+ marginTop: 4,
+ },
+ entityRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 12,
+ padding: 12,
+ borderRadius: 18,
+ borderWidth: 1,
+ marginBottom: 10,
+ },
+ logoDot: {
+ width: 34,
+ height: 34,
+ borderRadius: 14,
+ alignItems: "center",
+ justifyContent: "center",
+ borderWidth: 1,
+ },
+ logoText: {
+ fontSize: 12,
+ fontWeight: "900",
+ color: "rgba(255, 255, 255, 0.92)",
+ },
+ entityMeta: {
+ flex: 1,
+ minWidth: 0,
+ },
+ entityName: {
+ fontWeight: "900",
+ },
+ entitySub: {
+ fontSize: 12,
+ opacity: 0.55,
+ marginTop: 3,
+ },
+ entityBadge: {
+ fontSize: 12,
+ opacity: 0.52,
+ fontWeight: "900",
+ },
+ emptyState: {
+ padding: 14,
+ borderRadius: 20,
+ borderWidth: 1,
+ },
+ emptyTitle: {
+ fontWeight: "900",
+ },
+ emptySub: {
+ marginTop: 6,
+ opacity: 0.6,
+ fontSize: 12,
+ fontWeight: "800",
+ lineHeight: 18,
+ },
+ overlay: {
+ ...StyleSheet.absoluteFillObject,
+ zIndex: 80,
+ },
+ sheet: {
+ position: "absolute",
+ left: 0,
+ right: 0,
+ bottom: 0,
+ borderTopLeftRadius: 22,
+ borderTopRightRadius: 22,
+ borderWidth: 1,
+ padding: 12,
+ zIndex: 90,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: -10 },
+ shadowOpacity: 0.3,
+ shadowRadius: 30,
+ elevation: 20,
+ },
+ sheetHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: 10,
+ marginBottom: 12,
+ },
+ sheetTitleContainer: {
+ flex: 1,
+ minWidth: 0,
+ },
+ sheetTitle: {
+ fontWeight: "900",
+ },
+ sheetSub: {
+ fontSize: 12,
+ opacity: 0.52,
+ fontWeight: "800",
+ marginTop: 2,
+ },
+ kvContainer: {
+ flexDirection: "row",
+ flexWrap: "wrap",
+ gap: 10,
+ marginBottom: 12,
+ },
+ kvItem: {
+ flex: 1,
+ minWidth: "45%",
+ padding: 12,
+ borderRadius: 18,
+ borderWidth: 1,
+ },
+ kvLabel: {
+ fontSize: 12,
+ opacity: 0.52,
+ fontWeight: "900",
+ },
+ kvValue: {
+ marginTop: 6,
+ fontWeight: "900",
+ },
+ sheetActions: {
+ flexDirection: "row",
+ gap: 10,
+ },
+ sheetBtn: {
+ flex: 1,
+ height: 36,
+ paddingHorizontal: 12,
+ borderRadius: 14,
+ borderWidth: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ sheetBtnPrimary: {},
+ sheetBtnText: {
+ fontSize: 14,
+ fontWeight: "900",
+ opacity: 0.86,
+ },
+ sheetBtnTextPrimary: {
+ opacity: 1,
+ },
+});
diff --git a/components/home-header.tsx b/components/home-header.tsx
index 406144b..c7079e8 100644
--- a/components/home-header.tsx
+++ b/components/home-header.tsx
@@ -25,9 +25,7 @@ export function HomeHeader() {
{
- /* TODO: Search Action */
- }}
+ onPress={() => router.push("/search")}
>
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 98cdde6..0ded263 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -69,5 +69,31 @@
"badminton": "Badminton",
"snooker": "Snooker",
"volleyball": "Volleyball"
+ },
+ "search": {
+ "title": "Search",
+ "subtitle": "Teams · Players · Leagues",
+ "placeholder": "Search teams, players, leagues",
+ "recent": "Recent searches",
+ "clear": "Clear",
+ "results": "Results",
+ "found": "found",
+ "tap_to_open": "Tap to open",
+ "no_results": "No results",
+ "no_results_hint": "Try another keyword or switch category",
+ "type": {
+ "team": "Team",
+ "player": "Player",
+ "league": "League"
+ },
+ "detail": {
+ "country": "Country",
+ "league": "League",
+ "season": "Season",
+ "team": "Team",
+ "position": "Position",
+ "open": "Open detail",
+ "save": "Save to recent"
+ }
}
}
\ No newline at end of file
diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json
index dfb1b5a..a7b2a68 100644
--- a/i18n/locales/zh.json
+++ b/i18n/locales/zh.json
@@ -69,5 +69,31 @@
"badminton": "羽毛球",
"snooker": "斯诺克",
"volleyball": "排球"
+ },
+ "search": {
+ "title": "搜索",
+ "subtitle": "球队 · 球员 · 联赛",
+ "placeholder": "搜索球队、球员、联赛",
+ "recent": "最近搜索",
+ "clear": "清除",
+ "results": "结果",
+ "found": "个结果",
+ "tap_to_open": "点击打开",
+ "no_results": "无结果",
+ "no_results_hint": "尝试其他关键词或切换分类",
+ "type": {
+ "team": "球队",
+ "player": "球员",
+ "league": "联赛"
+ },
+ "detail": {
+ "country": "国家",
+ "league": "联赛",
+ "season": "赛季",
+ "team": "球队",
+ "position": "位置",
+ "open": "打开详情",
+ "save": "保存到最近"
+ }
}
}
\ No newline at end of file