无图片默认渐变色头像
This commit is contained in:
278
app/search.tsx
278
app/search.tsx
@@ -5,6 +5,7 @@ import { Colors } from "@/constants/theme";
|
||||
import { useAppState } from "@/context/AppStateContext";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { fetchSearch } from "@/lib/api";
|
||||
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
|
||||
import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { BlurView } from "expo-blur";
|
||||
@@ -78,65 +79,6 @@ 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 {
|
||||
@@ -171,6 +113,8 @@ export default function SearchScreen() {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const isDark = theme === "dark";
|
||||
const cardBg = isDark ? "#1C1C1E" : "#FFFFFF";
|
||||
const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)";
|
||||
const { state } = useAppState();
|
||||
|
||||
const [searchType, setSearchType] = useState<SearchType>("all");
|
||||
@@ -181,6 +125,7 @@ export default function SearchScreen() {
|
||||
const [entities, setEntities] = useState<SearchEntity[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
|
||||
const sheetAnim = useRef(new Animated.Value(0)).current;
|
||||
const searchTimeoutRef = useRef<number | null>(null);
|
||||
@@ -226,6 +171,7 @@ export default function SearchScreen() {
|
||||
if (!query.trim()) {
|
||||
setEntities([]);
|
||||
setError(null);
|
||||
setHasSearched(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,6 +191,7 @@ export default function SearchScreen() {
|
||||
];
|
||||
|
||||
setEntities(allEntities);
|
||||
setHasSearched(true);
|
||||
if (allEntities.length > 0) {
|
||||
const searchQuery = query.trim();
|
||||
setRecentSearches((prev) => {
|
||||
@@ -260,6 +207,7 @@ export default function SearchScreen() {
|
||||
console.error("Search error:", err);
|
||||
setError(err?.message || t("search.error"));
|
||||
setEntities([]);
|
||||
setHasSearched(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -328,12 +276,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.search,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(0, 0, 0, 0.28)"
|
||||
: "rgba(255, 255, 255, 0.8)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.12)"
|
||||
: "rgba(0, 0, 0, 0.1)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -368,20 +312,8 @@ export default function SearchScreen() {
|
||||
styles.chip,
|
||||
searchType === type && styles.chipActive,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? searchType === type
|
||||
? "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)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleSearchTypeChange(type)}
|
||||
@@ -426,12 +358,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.recentCard,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleRecentClick(item)}
|
||||
@@ -458,12 +386,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.entityRow,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.06)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleEntityPress(item)}
|
||||
@@ -503,12 +427,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.emptyState,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -547,12 +467,8 @@ export default function SearchScreen() {
|
||||
styles.sheet,
|
||||
{
|
||||
transform: [{ translateY }],
|
||||
backgroundColor: isDark
|
||||
? "rgba(18, 20, 26, 0.96)"
|
||||
: "rgba(255, 255, 255, 0.96)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.12)"
|
||||
: "rgba(0, 0, 0, 0.1)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
{ paddingBottom: insets.bottom + 14 },
|
||||
]}
|
||||
@@ -571,12 +487,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.iconBtn,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.06)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.10)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => setShowDetailSheet(false)}
|
||||
@@ -597,12 +509,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -619,12 +527,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -645,12 +549,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -667,12 +567,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -689,12 +585,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -711,12 +603,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -737,12 +625,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.kvItem,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.05)"
|
||||
: "rgba(0, 0, 0, 0.04)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.08)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -763,12 +647,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.sheetBtn,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(0, 0, 0, 0.14)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.12)"
|
||||
: "rgba(0, 0, 0, 0.1)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={handleOpenDetail}
|
||||
@@ -822,9 +702,7 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.nav,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(7, 8, 11, 0.95)"
|
||||
: "rgba(255, 255, 255, 0.95)",
|
||||
backgroundColor: cardBg,
|
||||
},
|
||||
{ paddingTop: insets.top + 10 },
|
||||
]}
|
||||
@@ -833,12 +711,8 @@ export default function SearchScreen() {
|
||||
style={[
|
||||
styles.iconBtn,
|
||||
{
|
||||
backgroundColor: isDark
|
||||
? "rgba(255, 255, 255, 0.06)"
|
||||
: "rgba(0, 0, 0, 0.06)",
|
||||
borderColor: isDark
|
||||
? "rgba(255, 255, 255, 0.10)"
|
||||
: "rgba(0, 0, 0, 0.10)",
|
||||
backgroundColor: cardBg,
|
||||
borderColor: borderColor,
|
||||
},
|
||||
]}
|
||||
onPress={() => router.back()}
|
||||
@@ -892,12 +766,13 @@ export default function SearchScreen() {
|
||||
{t("search.error_hint")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
) : hasSearched && filteredEntities.length === 0 ? (
|
||||
renderEmptyState()
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredEntities}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderEntityItem}
|
||||
ListEmptyComponent={() => renderEmptyState()}
|
||||
contentContainerStyle={[
|
||||
styles.listContent,
|
||||
{ paddingBottom: insets.bottom + 20 },
|
||||
@@ -942,13 +817,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
brandTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "900",
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
brandSub: {
|
||||
fontSize: 12,
|
||||
opacity: 0.42,
|
||||
fontWeight: "800",
|
||||
opacity: 0.5,
|
||||
fontWeight: "600",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
@@ -993,9 +868,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
chipActive: {},
|
||||
chipText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "900",
|
||||
opacity: 0.78,
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
opacity: 0.8,
|
||||
},
|
||||
chipTextActive: {
|
||||
opacity: 1,
|
||||
@@ -1008,14 +883,14 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 13,
|
||||
opacity: 0.55,
|
||||
fontWeight: "900",
|
||||
fontSize: 12,
|
||||
opacity: 0.8,
|
||||
fontWeight: "700",
|
||||
},
|
||||
sectionAction: {
|
||||
fontSize: 12,
|
||||
opacity: 0.42,
|
||||
fontWeight: "800",
|
||||
opacity: 0.5,
|
||||
fontWeight: "600",
|
||||
},
|
||||
recentRow: {
|
||||
marginBottom: 8,
|
||||
@@ -1031,12 +906,14 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
},
|
||||
recentCardTitle: {
|
||||
fontWeight: "900",
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
recentCardSub: {
|
||||
fontSize: 12,
|
||||
opacity: 0.55,
|
||||
opacity: 0.5,
|
||||
marginTop: 4,
|
||||
fontWeight: "500",
|
||||
},
|
||||
entityRow: {
|
||||
flexDirection: "row",
|
||||
@@ -1057,7 +934,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 12,
|
||||
fontWeight: "900",
|
||||
fontWeight: "700",
|
||||
color: "rgba(255, 255, 255, 0.92)",
|
||||
},
|
||||
entityMeta: {
|
||||
@@ -1065,17 +942,19 @@ const styles = StyleSheet.create({
|
||||
minWidth: 0,
|
||||
},
|
||||
entityName: {
|
||||
fontWeight: "900",
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
entitySub: {
|
||||
fontSize: 12,
|
||||
opacity: 0.55,
|
||||
opacity: 0.5,
|
||||
marginTop: 3,
|
||||
fontWeight: "500",
|
||||
},
|
||||
entityBadge: {
|
||||
fontSize: 12,
|
||||
opacity: 0.52,
|
||||
fontWeight: "900",
|
||||
fontSize: 10,
|
||||
opacity: 0.9,
|
||||
fontWeight: "700",
|
||||
},
|
||||
center: {
|
||||
flex: 1,
|
||||
@@ -1089,13 +968,14 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 1,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontWeight: "900",
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
emptySub: {
|
||||
marginTop: 6,
|
||||
opacity: 0.6,
|
||||
opacity: 0.5,
|
||||
fontSize: 12,
|
||||
fontWeight: "800",
|
||||
fontWeight: "500",
|
||||
lineHeight: 18,
|
||||
},
|
||||
overlay: {
|
||||
@@ -1130,12 +1010,13 @@ const styles = StyleSheet.create({
|
||||
minWidth: 0,
|
||||
},
|
||||
sheetTitle: {
|
||||
fontWeight: "900",
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
sheetSub: {
|
||||
fontSize: 12,
|
||||
opacity: 0.52,
|
||||
fontWeight: "800",
|
||||
opacity: 0.5,
|
||||
fontWeight: "500",
|
||||
marginTop: 2,
|
||||
},
|
||||
kvContainer: {
|
||||
@@ -1153,12 +1034,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
kvLabel: {
|
||||
fontSize: 12,
|
||||
opacity: 0.52,
|
||||
fontWeight: "900",
|
||||
opacity: 0.5,
|
||||
fontWeight: "700",
|
||||
},
|
||||
kvValue: {
|
||||
marginTop: 6,
|
||||
fontWeight: "900",
|
||||
fontSize: 12,
|
||||
fontWeight: "600",
|
||||
},
|
||||
sheetActions: {
|
||||
flexDirection: "row",
|
||||
@@ -1175,9 +1057,9 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
sheetBtnPrimary: {},
|
||||
sheetBtnText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "900",
|
||||
opacity: 0.86,
|
||||
fontSize: 12,
|
||||
fontWeight: "700",
|
||||
opacity: 0.9,
|
||||
},
|
||||
sheetBtnTextPrimary: {
|
||||
opacity: 1,
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Colors } from "@/constants/theme";
|
||||
import { useAppState } from "@/context/AppStateContext";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { addFavorite, removeFavorite } from "@/lib/api";
|
||||
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
|
||||
import { Match } from "@/types/api";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
@@ -179,14 +181,31 @@ export function MatchCardLeague({
|
||||
|
||||
<View style={styles.teamsColumn}>
|
||||
<View style={styles.teamRow}>
|
||||
<Image
|
||||
source={{
|
||||
uri: isTennis
|
||||
{(() => {
|
||||
const teamName = isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName);
|
||||
const logoUri = isTennis
|
||||
? (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");
|
||||
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}>
|
||||
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
|
||||
{isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName)}
|
||||
@@ -197,14 +216,31 @@ export function MatchCardLeague({
|
||||
</View>
|
||||
|
||||
<View style={[styles.teamRow, { marginTop: 10 }]}>
|
||||
<Image
|
||||
source={{
|
||||
uri: isTennis
|
||||
{(() => {
|
||||
const teamName = isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName);
|
||||
const logoUri = isTennis
|
||||
? (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");
|
||||
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}>
|
||||
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
|
||||
{isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName)}
|
||||
@@ -310,13 +346,26 @@ const styles = StyleSheet.create({
|
||||
marginRight: 12,
|
||||
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: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
teamName: {
|
||||
fontSize: 15,
|
||||
fontSize: 13,
|
||||
fontWeight: "500",
|
||||
},
|
||||
cardBadge: {
|
||||
|
||||
@@ -3,8 +3,10 @@ import { ThemedText } from "@/components/themed-text";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { fetchLeagues, fetchTodayMatches } from "@/lib/api";
|
||||
import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
|
||||
import { League, Match } from "@/types/api";
|
||||
import { Image } from "expo-image";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -208,12 +210,27 @@ export function MatchesByLeague({
|
||||
style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]}
|
||||
>
|
||||
<View style={styles.leagueHeaderLeft}>
|
||||
{(() => {
|
||||
const hasLogo = league.logo && league.logo.trim() !== "" && !league.logo.includes("placehold");
|
||||
const gradient = getLogoGradient(league.name || "");
|
||||
const initials = getInitials(league.name || "");
|
||||
|
||||
return hasLogo ? (
|
||||
<Image
|
||||
source={{
|
||||
uri: league.logo || "https://placehold.co/40x40/png",
|
||||
}}
|
||||
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}>
|
||||
<ThemedText
|
||||
@@ -326,6 +343,20 @@ const styles = StyleSheet.create({
|
||||
marginRight: 12,
|
||||
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: {
|
||||
justifyContent: "center",
|
||||
},
|
||||
|
||||
55
lib/avatar-utils.ts
Normal file
55
lib/avatar-utils.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user