无图片默认渐变色头像

This commit is contained in:
xianyi
2026-01-22 10:52:17 +08:00
parent cf9604d73e
commit 9e7f8dadec
4 changed files with 242 additions and 225 deletions

View File

@@ -5,6 +5,7 @@ import { Colors } from "@/constants/theme";
import { useAppState } from "@/context/AppStateContext"; import { useAppState } from "@/context/AppStateContext";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { fetchSearch } from "@/lib/api"; import { fetchSearch } from "@/lib/api";
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api"; import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
@@ -78,71 +79,12 @@ function convertTeamToEntity(team: SearchTeam): SearchEntity {
}; };
} }
// 生成首字母
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 { function getSubText(entity: SearchEntity): string {
const { meta } = entity; const { meta } = entity;
const parts: string[] = []; const parts: string[] = [];
if (entity.type === "league") { if (entity.type === "league") {
// 联赛:显示国家 // 联赛:显示国家
if (meta.country) parts.push(meta.country); if (meta.country) parts.push(meta.country);
@@ -159,7 +101,7 @@ function getSubText(entity: SearchEntity): string {
if (meta.country) parts.push(meta.country); if (meta.country) parts.push(meta.country);
if (meta.league) parts.push(meta.league); if (meta.league) parts.push(meta.league);
} }
return parts.filter(Boolean).join(" · ") || ""; return parts.filter(Boolean).join(" · ") || "";
} }
@@ -171,6 +113,8 @@ export default function SearchScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isDark = theme === "dark"; const isDark = theme === "dark";
const cardBg = isDark ? "#1C1C1E" : "#FFFFFF";
const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)";
const { state } = useAppState(); const { state } = useAppState();
const [searchType, setSearchType] = useState<SearchType>("all"); const [searchType, setSearchType] = useState<SearchType>("all");
@@ -181,6 +125,7 @@ export default function SearchScreen() {
const [entities, setEntities] = useState<SearchEntity[]>([]); const [entities, setEntities] = useState<SearchEntity[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [hasSearched, setHasSearched] = useState(false);
const sheetAnim = useRef(new Animated.Value(0)).current; const sheetAnim = useRef(new Animated.Value(0)).current;
const searchTimeoutRef = useRef<number | null>(null); const searchTimeoutRef = useRef<number | null>(null);
@@ -226,6 +171,7 @@ export default function SearchScreen() {
if (!query.trim()) { if (!query.trim()) {
setEntities([]); setEntities([]);
setError(null); setError(null);
setHasSearched(false);
return; return;
} }
@@ -245,6 +191,7 @@ export default function SearchScreen() {
]; ];
setEntities(allEntities); setEntities(allEntities);
setHasSearched(true);
if (allEntities.length > 0) { if (allEntities.length > 0) {
const searchQuery = query.trim(); const searchQuery = query.trim();
setRecentSearches((prev) => { setRecentSearches((prev) => {
@@ -260,6 +207,7 @@ export default function SearchScreen() {
console.error("Search error:", err); console.error("Search error:", err);
setError(err?.message || t("search.error")); setError(err?.message || t("search.error"));
setEntities([]); setEntities([]);
setHasSearched(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -328,12 +276,8 @@ export default function SearchScreen() {
style={[ style={[
styles.search, styles.search,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(0, 0, 0, 0.28)" borderColor: borderColor,
: "rgba(255, 255, 255, 0.8)",
borderColor: isDark
? "rgba(255, 255, 255, 0.12)"
: "rgba(0, 0, 0, 0.1)",
}, },
]} ]}
> >
@@ -368,20 +312,8 @@ export default function SearchScreen() {
styles.chip, styles.chip,
searchType === type && styles.chipActive, searchType === type && styles.chipActive,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? searchType === type borderColor: borderColor,
? "rgba(255, 255, 255, 0.12)"
: "rgba(255, 255, 255, 0.06)"
: searchType === type
? "rgba(0, 0, 0, 0.08)"
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? searchType === type
? "rgba(255, 255, 255, 0.14)"
: "rgba(255, 255, 255, 0.10)"
: searchType === type
? "rgba(0, 0, 0, 0.12)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
onPress={() => handleSearchTypeChange(type)} onPress={() => handleSearchTypeChange(type)}
@@ -426,12 +358,8 @@ export default function SearchScreen() {
style={[ style={[
styles.recentCard, styles.recentCard,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
onPress={() => handleRecentClick(item)} onPress={() => handleRecentClick(item)}
@@ -458,12 +386,8 @@ export default function SearchScreen() {
style={[ style={[
styles.entityRow, styles.entityRow,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.06)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
onPress={() => handleEntityPress(item)} onPress={() => handleEntityPress(item)}
@@ -503,12 +427,8 @@ export default function SearchScreen() {
style={[ style={[
styles.emptyState, styles.emptyState,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -547,12 +467,8 @@ export default function SearchScreen() {
styles.sheet, styles.sheet,
{ {
transform: [{ translateY }], transform: [{ translateY }],
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(18, 20, 26, 0.96)" borderColor: borderColor,
: "rgba(255, 255, 255, 0.96)",
borderColor: isDark
? "rgba(255, 255, 255, 0.12)"
: "rgba(0, 0, 0, 0.1)",
}, },
{ paddingBottom: insets.bottom + 14 }, { paddingBottom: insets.bottom + 14 },
]} ]}
@@ -571,12 +487,8 @@ export default function SearchScreen() {
style={[ style={[
styles.iconBtn, styles.iconBtn,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.06)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.10)",
}, },
]} ]}
onPress={() => setShowDetailSheet(false)} onPress={() => setShowDetailSheet(false)}
@@ -597,12 +509,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -619,12 +527,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -645,12 +549,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -667,12 +567,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -689,12 +585,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -711,12 +603,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -737,12 +625,8 @@ export default function SearchScreen() {
style={[ style={[
styles.kvItem, styles.kvItem,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.05)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.04)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.08)",
}, },
]} ]}
> >
@@ -763,12 +647,8 @@ export default function SearchScreen() {
style={[ style={[
styles.sheetBtn, styles.sheetBtn,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(0, 0, 0, 0.14)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.12)"
: "rgba(0, 0, 0, 0.1)",
}, },
]} ]}
onPress={handleOpenDetail} onPress={handleOpenDetail}
@@ -822,9 +702,7 @@ export default function SearchScreen() {
style={[ style={[
styles.nav, styles.nav,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(7, 8, 11, 0.95)"
: "rgba(255, 255, 255, 0.95)",
}, },
{ paddingTop: insets.top + 10 }, { paddingTop: insets.top + 10 },
]} ]}
@@ -833,12 +711,8 @@ export default function SearchScreen() {
style={[ style={[
styles.iconBtn, styles.iconBtn,
{ {
backgroundColor: isDark backgroundColor: cardBg,
? "rgba(255, 255, 255, 0.06)" borderColor: borderColor,
: "rgba(0, 0, 0, 0.06)",
borderColor: isDark
? "rgba(255, 255, 255, 0.10)"
: "rgba(0, 0, 0, 0.10)",
}, },
]} ]}
onPress={() => router.back()} onPress={() => router.back()}
@@ -873,8 +747,8 @@ export default function SearchScreen() {
{loading {loading
? t("search.loading") ? t("search.loading")
: error : error
? t("search.error") ? t("search.error")
: `${filteredEntities.length} ${t("search.found")}`} : `${filteredEntities.length} ${t("search.found")}`}
</ThemedText> </ThemedText>
</View> </View>
)} )}
@@ -892,12 +766,13 @@ export default function SearchScreen() {
{t("search.error_hint")} {t("search.error_hint")}
</ThemedText> </ThemedText>
</View> </View>
) : hasSearched && filteredEntities.length === 0 ? (
renderEmptyState()
) : ( ) : (
<FlatList <FlatList
data={filteredEntities} data={filteredEntities}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={renderEntityItem} renderItem={renderEntityItem}
ListEmptyComponent={() => renderEmptyState()}
contentContainerStyle={[ contentContainerStyle={[
styles.listContent, styles.listContent,
{ paddingBottom: insets.bottom + 20 }, { paddingBottom: insets.bottom + 20 },
@@ -942,13 +817,13 @@ const styles = StyleSheet.create({
}, },
brandTitle: { brandTitle: {
fontSize: 18, fontSize: 18,
fontWeight: "900", fontWeight: "700",
letterSpacing: 0.2, letterSpacing: 0.2,
}, },
brandSub: { brandSub: {
fontSize: 12, fontSize: 12,
opacity: 0.42, opacity: 0.5,
fontWeight: "800", fontWeight: "600",
}, },
content: { content: {
flex: 1, flex: 1,
@@ -993,9 +868,9 @@ const styles = StyleSheet.create({
}, },
chipActive: {}, chipActive: {},
chipText: { chipText: {
fontSize: 14, fontSize: 12,
fontWeight: "900", fontWeight: "700",
opacity: 0.78, opacity: 0.8,
}, },
chipTextActive: { chipTextActive: {
opacity: 1, opacity: 1,
@@ -1008,14 +883,14 @@ const styles = StyleSheet.create({
marginBottom: 8, marginBottom: 8,
}, },
sectionTitle: { sectionTitle: {
fontSize: 13, fontSize: 12,
opacity: 0.55, opacity: 0.8,
fontWeight: "900", fontWeight: "700",
}, },
sectionAction: { sectionAction: {
fontSize: 12, fontSize: 12,
opacity: 0.42, opacity: 0.5,
fontWeight: "800", fontWeight: "600",
}, },
recentRow: { recentRow: {
marginBottom: 8, marginBottom: 8,
@@ -1031,12 +906,14 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
}, },
recentCardTitle: { recentCardTitle: {
fontWeight: "900", fontSize: 12,
fontWeight: "600",
}, },
recentCardSub: { recentCardSub: {
fontSize: 12, fontSize: 12,
opacity: 0.55, opacity: 0.5,
marginTop: 4, marginTop: 4,
fontWeight: "500",
}, },
entityRow: { entityRow: {
flexDirection: "row", flexDirection: "row",
@@ -1057,7 +934,7 @@ const styles = StyleSheet.create({
}, },
logoText: { logoText: {
fontSize: 12, fontSize: 12,
fontWeight: "900", fontWeight: "700",
color: "rgba(255, 255, 255, 0.92)", color: "rgba(255, 255, 255, 0.92)",
}, },
entityMeta: { entityMeta: {
@@ -1065,17 +942,19 @@ const styles = StyleSheet.create({
minWidth: 0, minWidth: 0,
}, },
entityName: { entityName: {
fontWeight: "900", fontSize: 12,
fontWeight: "600",
}, },
entitySub: { entitySub: {
fontSize: 12, fontSize: 12,
opacity: 0.55, opacity: 0.5,
marginTop: 3, marginTop: 3,
fontWeight: "500",
}, },
entityBadge: { entityBadge: {
fontSize: 12, fontSize: 10,
opacity: 0.52, opacity: 0.9,
fontWeight: "900", fontWeight: "700",
}, },
center: { center: {
flex: 1, flex: 1,
@@ -1089,13 +968,14 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
}, },
emptyTitle: { emptyTitle: {
fontWeight: "900", fontSize: 12,
fontWeight: "600",
}, },
emptySub: { emptySub: {
marginTop: 6, marginTop: 6,
opacity: 0.6, opacity: 0.5,
fontSize: 12, fontSize: 12,
fontWeight: "800", fontWeight: "500",
lineHeight: 18, lineHeight: 18,
}, },
overlay: { overlay: {
@@ -1130,12 +1010,13 @@ const styles = StyleSheet.create({
minWidth: 0, minWidth: 0,
}, },
sheetTitle: { sheetTitle: {
fontWeight: "900", fontSize: 12,
fontWeight: "600",
}, },
sheetSub: { sheetSub: {
fontSize: 12, fontSize: 12,
opacity: 0.52, opacity: 0.5,
fontWeight: "800", fontWeight: "500",
marginTop: 2, marginTop: 2,
}, },
kvContainer: { kvContainer: {
@@ -1153,12 +1034,13 @@ const styles = StyleSheet.create({
}, },
kvLabel: { kvLabel: {
fontSize: 12, fontSize: 12,
opacity: 0.52, opacity: 0.5,
fontWeight: "900", fontWeight: "700",
}, },
kvValue: { kvValue: {
marginTop: 6, marginTop: 6,
fontWeight: "900", fontSize: 12,
fontWeight: "600",
}, },
sheetActions: { sheetActions: {
flexDirection: "row", flexDirection: "row",
@@ -1175,9 +1057,9 @@ const styles = StyleSheet.create({
}, },
sheetBtnPrimary: {}, sheetBtnPrimary: {},
sheetBtnText: { sheetBtnText: {
fontSize: 14, fontSize: 12,
fontWeight: "900", fontWeight: "700",
opacity: 0.86, opacity: 0.9,
}, },
sheetBtnTextPrimary: { sheetBtnTextPrimary: {
opacity: 1, opacity: 1,

View File

@@ -4,7 +4,9 @@ import { Colors } from "@/constants/theme";
import { useAppState } from "@/context/AppStateContext"; import { useAppState } from "@/context/AppStateContext";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { addFavorite, removeFavorite } from "@/lib/api"; import { addFavorite, removeFavorite } from "@/lib/api";
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
import { Match } from "@/types/api"; import { Match } from "@/types/api";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import React, { useState } from "react"; import React, { useState } from "react";
import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native"; import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
@@ -179,14 +181,31 @@ export function MatchCardLeague({
<View style={styles.teamsColumn}> <View style={styles.teamsColumn}>
<View style={styles.teamRow}> <View style={styles.teamRow}>
<Image {(() => {
source={{ const teamName = isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName);
uri: isTennis const logoUri = isTennis
? (match as any).eventFirstPlayerLogo ? (match as any).eventFirstPlayerLogo
: ((match as any).homeLogo || match.homeTeamLogo || "https://placehold.co/24x24/png") : ((match as any).homeLogo || match.homeTeamLogo);
}} const hasLogo = logoUri && logoUri.trim() !== "" && !logoUri.includes("placehold");
style={styles.teamLogo} const gradient = getLogoGradient(teamName || "");
/> const initials = getInitials(teamName || "");
return hasLogo ? (
<Image
source={{ uri: logoUri }}
style={styles.teamLogo}
/>
) : (
<LinearGradient
colors={[gradient.color1, gradient.color2]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.teamLogoGradient}
>
<ThemedText style={styles.teamLogoText}>{initials}</ThemedText>
</LinearGradient>
);
})()}
<View style={styles.teamNameContainer}> <View style={styles.teamNameContainer}>
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}> <ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
{isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName)} {isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName)}
@@ -197,14 +216,31 @@ export function MatchCardLeague({
</View> </View>
<View style={[styles.teamRow, { marginTop: 10 }]}> <View style={[styles.teamRow, { marginTop: 10 }]}>
<Image {(() => {
source={{ const teamName = isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName);
uri: isTennis const logoUri = isTennis
? (match as any).eventSecondPlayerLogo ? (match as any).eventSecondPlayerLogo
: ((match as any).awayLogo || match.awayTeamLogo || "https://placehold.co/24x24/png") : ((match as any).awayLogo || match.awayTeamLogo);
}} const hasLogo = logoUri && logoUri.trim() !== "" && !logoUri.includes("placehold");
style={styles.teamLogo} const gradient = getLogoGradient(teamName || "");
/> const initials = getInitials(teamName || "");
return hasLogo ? (
<Image
source={{ uri: logoUri }}
style={styles.teamLogo}
/>
) : (
<LinearGradient
colors={[gradient.color1, gradient.color2]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.teamLogoGradient}
>
<ThemedText style={styles.teamLogoText}>{initials}</ThemedText>
</LinearGradient>
);
})()}
<View style={styles.teamNameContainer}> <View style={styles.teamNameContainer}>
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}> <ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
{isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName)} {isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName)}
@@ -310,13 +346,26 @@ const styles = StyleSheet.create({
marginRight: 12, marginRight: 12,
backgroundColor: "#E5E5E5", backgroundColor: "#E5E5E5",
}, },
teamLogoGradient: {
width: 24,
height: 24,
borderRadius: 14,
marginRight: 12,
alignItems: "center",
justifyContent: "center",
},
teamLogoText: {
fontSize: 9,
fontWeight: "700",
color: "rgba(255, 255, 255, 0.92)",
},
teamNameContainer: { teamNameContainer: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
flex: 1, flex: 1,
}, },
teamName: { teamName: {
fontSize: 15, fontSize: 13,
fontWeight: "500", fontWeight: "500",
}, },
cardBadge: { cardBadge: {

View File

@@ -3,8 +3,10 @@ import { ThemedText } from "@/components/themed-text";
import { Colors } from "@/constants/theme"; import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import { fetchLeagues, fetchTodayMatches } from "@/lib/api"; import { fetchLeagues, fetchTodayMatches } from "@/lib/api";
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
import { League, Match } from "@/types/api"; import { League, Match } from "@/types/api";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import React, { useState } from "react"; import React, { useState } from "react";
import { import {
ActivityIndicator, ActivityIndicator,
@@ -208,12 +210,27 @@ export function MatchesByLeague({
style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]} style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]}
> >
<View style={styles.leagueHeaderLeft}> <View style={styles.leagueHeaderLeft}>
<Image {(() => {
source={{ const hasLogo = league.logo && league.logo.trim() !== "" && !league.logo.includes("placehold");
uri: league.logo || "https://placehold.co/40x40/png", const gradient = getLogoGradient(league.name || "");
}} const initials = getInitials(league.name || "");
style={[styles.leagueLogo, { backgroundColor: isDark ? "#3A3A3C" : "#E5E5E5" }]}
/> return hasLogo ? (
<Image
source={{ uri: league.logo }}
style={[styles.leagueLogo, { backgroundColor: isDark ? "#3A3A3C" : "#E5E5E5" }]}
/>
) : (
<LinearGradient
colors={[gradient.color1, gradient.color2]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.leagueLogoGradient}
>
<ThemedText style={styles.leagueLogoText}>{initials}</ThemedText>
</LinearGradient>
);
})()}
<View style={styles.leagueInfoText}> <View style={styles.leagueInfoText}>
<ThemedText <ThemedText
@@ -326,6 +343,20 @@ const styles = StyleSheet.create({
marginRight: 12, marginRight: 12,
marginLeft: 14, marginLeft: 14,
}, },
leagueLogoGradient: {
width: 24,
height: 24,
borderRadius: 8,
marginRight: 12,
marginLeft: 14,
alignItems: "center",
justifyContent: "center",
},
leagueLogoText: {
fontSize: 9,
fontWeight: "700",
color: "rgba(255, 255, 255, 0.92)",
},
leagueInfoText: { leagueInfoText: {
justifyContent: "center", justifyContent: "center",
}, },

55
lib/avatar-utils.ts Normal file
View File

@@ -0,0 +1,55 @@
export 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;
}
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})`;
}
export 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),
};
}