You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1119 lines
32 KiB

import React, { useState, useEffect, useCallback, useRef } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
Image,
TouchableOpacity,
TextInput,
SafeAreaView,
StatusBar,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
ScrollView,
} from "react-native";
import Ionicons from "@expo/vector-icons/Ionicons";
import { useNavigation, useRoute } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { RouteProp } from "@react-navigation/native";
import {
productApi,
ProductParams,
type Product,
} from "../services/api/productApi";
import { useTranslation } from "react-i18next";
import isSmallScreen from "../utils/isSmallScreen";
import { Svg, Path } from "react-native-svg";
import SearchIcon from "../components/SearchIcon";
import widthUtils from "../utils/widthUtils";
import fontSize from "../utils/fontsizeUtils";
import useUserStore from "../store/user";
import { getSubjectTransLanguage } from "../utils/languageUtils";
// 图标组件 - 使用React.memo优化渲染
const IconComponent = React.memo(
({ name, size, color }: { name: string; size: number; color: string }) => {
const Icon = Ionicons as any;
return <Icon name={name} size={size} color={color} />;
}
);
// 路由参数类型
type SearchResultRouteParams = {
keyword: string;
};
// 组件Props类型
type SearchResultScreenProps = {
route: RouteProp<Record<string, SearchResultRouteParams>, string>;
navigation: NativeStackNavigationProp<any>;
};
// 懒加载图片组件 - 改进版本
const LazyImage = React.memo(
({
uri,
style,
resizeMode,
}: {
uri: string;
style: any;
resizeMode: any;
}) => {
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
const onLoad = useCallback(() => {
setIsLoaded(true);
}, []);
const onError = useCallback(() => {
setHasError(true);
setIsLoaded(true); // Also mark as loaded on error to remove placeholder
}, []);
return (
<View style={[style, { overflow: "hidden" }]}>
{/* Show placeholder while image is loading */}
{!isLoaded && !hasError && (
<View style={[style, styles.imagePlaceholder, { position: 'absolute', zIndex: 1 }]} />
)}
{/* Show error state if image failed to load */}
{hasError && (
<View
style={[style, styles.imagePlaceholder, { position: 'absolute', zIndex: 1 }]}
>
<IconComponent name="image-outline" size={24} color="#999" />
<Text style={{ fontSize: fontSize(12), color: "#999", marginTop: 4 }}>
</Text>
</View>
)}
{/* Actual image */}
<Image
source={{ uri }}
style={[style, { opacity: isLoaded ? 1 : 0 }]}
resizeMode={resizeMode}
onLoad={onLoad}
onError={onError}
/>
</View>
);
}
);
// 产品骨架屏组件 - 用于加载状态
const ProductSkeleton = React.memo(() => (
<View style={styles.productCard}>
<View style={[styles.productImageContainer, styles.imagePlaceholder]} />
<View style={styles.productInfo}>
<View style={[styles.skeletonText, { width: '90%', height: 16, marginBottom: 8 }]} />
<View style={[styles.skeletonText, { width: '70%', height: 16, marginBottom: 8 }]} />
<View style={[styles.skeletonText, { width: '40%', height: 24, marginBottom: 4 }]} />
<View style={[styles.skeletonText, { width: '30%', height: 12 }]} />
</View>
</View>
));
// 产品项组件 - 使用React.memo优化渲染
const ProductItem = React.memo(
({
product,
onPress,
t,
userStore,
}: {
product: Product;
onPress: (product: Product) => void;
t: any;
userStore: any;
}) => (
<TouchableOpacity
style={styles.productCard}
onPress={() => onPress(product)}
activeOpacity={0.7}
key={product.offer_id}
>
<View style={styles.productImageContainer}>
{product.product_image_urls[0] ? (
<LazyImage
uri={product.product_image_urls[0]}
style={styles.productImage}
resizeMode="cover"
/>
) : (
<Text style={styles.placeholderText}>{t('productPicture')}</Text>
)}
{userStore.user?.user_id && (
<TouchableOpacity style={styles.vipIcon}>
<Text style={styles.vipButtonText}>VIP</Text>
<Text style={styles.vipLabelBold}>{userStore.user?.vip_level}</Text>
</TouchableOpacity>
)}
</View>
{/* 产品分类 */}
<View style={styles.productInfo}>
<Text style={styles.categoryText} numberOfLines={2}>
{getSubjectTransLanguage(product)}
</Text>
{/* 价格信息 */}
<View style={styles.beautyProductInfoRow}>
<View style={styles.flexRowCentered}>
{userStore.user?.user_id && (
<Text style={styles.priceLabel1}>
{product.original_min_price || "0"}
{product.currency || "FCFA"}
</Text>
)}
<View style={styles.priceContainer}>
<Text style={styles.highlightedText}>
{product.min_price || "0"}
</Text>
<Text style={styles.highlightedText1}>
{product.currency || "FCFA"}
</Text>
</View>
</View>
</View>
{/* 销售量 */}
<Text style={styles.productSales}>
{product.sold_out} {product.sold_out === 0 ? "" : "+"}{" "}
{t('sales')}
</Text>
</View>
</TouchableOpacity>
)
);
export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProps) => {
const { t } = useTranslation();
const [searchText, setSearchText] = useState("");
const [products, setProducts] = useState<Product[]>([]);
const [originalProducts, setOriginalProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null);
const [sortField, setSortField] = useState<"price" | "time">("price");
const [showBackToTop, setShowBackToTop] = useState(false);
const flatListRef = useRef<FlatList>(null);
const [activeTab, setActiveTab] = useState<"default" | "volume" | "price">(
"default"
);
const userStore = useUserStore();
const [showSkeleton, setShowSkeleton] = useState(true);
const [searchParams, setSearchParams] = useState<ProductParams>({
keyword: route.params?.keyword || "",
page: 1,
page_size: 10,
sort_order: "desc",
category_id: null,
sort_by: "default",
language: "en",
user_id: userStore.user.user_id,
});
// 初始化搜索关键字
useEffect(() => {
if (route.params?.keyword) {
setSearchText(route.params.keyword);
setShowSkeleton(true);
const newParams = {
...searchParams,
keyword: route.params.keyword,
};
setSearchParams(newParams);
searchProducts(newParams);
}
}, [route.params?.keyword]);
// 搜索产品的API调用
const searchProducts = useCallback(
async (params: ProductParams, isLoadMore = false) => {
if (!isLoadMore) {
setLoading(true);
setShowSkeleton(true);
} else {
setLoadingMore(true);
}
try {
const res = await productApi.getSearchProducts(params);
if (isLoadMore) {
setProducts((prev) => [...prev, ...res.products]);
} else {
setProducts(res.products);
// 保存原始排序的数据,以便默认排序时恢复
setOriginalProducts(res.products);
}
// 如果返回的数据少于页面大小,说明没有更多数据了
setHasMore(res.products.length === params.page_size);
} catch (error) {
console.error("Error searching products:", error);
// 发生错误时,设置hasMore为false,防止继续加载
setHasMore(false);
// 如果不是加载更多,清空产品列表
if (!isLoadMore) {
setProducts([]);
setOriginalProducts([]);
}
} finally {
setLoading(false);
setLoadingMore(false);
// Add a short delay before hiding skeletons for smoother transition
if (!isLoadMore) {
setTimeout(() => {
setShowSkeleton(false);
}, 300);
}
}
},
[]
);
// 处理搜索提交
const handleSearch = useCallback(() => {
if (searchText.trim()) {
// 重置排序状态
setSortField("price");
setSortOrder(null);
// 重置到默认标签
setActiveTab("default");
// Show skeleton for new search
setShowSkeleton(true);
const newParams = {
...searchParams,
keyword: searchText.trim(),
page: 1, // 重置到第一页
};
setSearchParams(newParams);
searchProducts(newParams);
}
}, [searchText, searchParams, searchProducts]);
// 切换筛选器显示状态
const toggleFilter = useCallback(() => {
setIsFilterVisible(!isFilterVisible);
}, [isFilterVisible]);
// 处理点击产品
const handleProductPress = useCallback(
(product: Product) => {
// 导航到产品详情页,并传递产品信息
navigation.navigate("ProductDetail", {
offer_id: product.offer_id,
searchKeyword: searchText,
price: product.min_price,
});
},
[navigation, searchText]
);
// 返回上一页
const goBack = useCallback(() => {
navigation.goBack();
}, [navigation]);
// 渲染列表为空时的组件
const renderEmptyList = useCallback(
() => (
<View style={styles.emptyContainer}>
<IconComponent name="search-outline" size={48} color="#ccc" />
<Text style={styles.emptyText}>
{t("noResults")} "{searchText}"
</Text>
<Text style={styles.emptySubtext}>{t("tryDifferentKeywords")}</Text>
</View>
),
[searchText, t]
);
// 渲染产品项
const renderProductItem = useCallback(
({ item }: { item: Product }) => (
<ProductItem
product={item}
onPress={handleProductPress}
t={t}
userStore={userStore}
/>
),
[handleProductPress, t, userStore]
);
// 创建产品列表项的key提取器
const keyExtractor = useCallback(
(item: Product, index: number) => `${item.offer_id}-${index}`,
[]
);
// 处理排序
const handleSort = useCallback(
(field: "price" | "time", order: "asc" | "desc") => {
setSortField(field);
setSortOrder(order);
// 本地排序,不发送API请求
setProducts((prevProducts) => {
const sortedProducts = [...prevProducts];
if (field === "price") {
sortedProducts.sort((a, b) => {
const priceA = a.min_price || 0;
const priceB = b.min_price || 0;
return order === "asc" ? priceA - priceB : priceB - priceA;
});
} else if (field === "time") {
sortedProducts.sort((a, b) => {
// 假设产品有create_time字段,如果没有可以使用其他时间相关字段
const timeA = new Date(a.create_date || 0).getTime();
const timeB = new Date(b.create_date || 0).getTime();
return order === "asc" ? timeA - timeB : timeB - timeA;
});
}
return sortedProducts;
});
},
[]
);
// 处理加载更多
const handleLoadMore = useCallback(() => {
if (loading || !hasMore || loadingMore) return;
setLoadingMore(true);
const newParams = {
...searchParams,
page: searchParams.page + 1,
};
setSearchParams(newParams);
searchProducts(newParams, true);
}, [loading, hasMore, loadingMore, searchParams, searchProducts]);
// 渲染底部加载指示器
const renderFooter = useCallback(() => {
if (!hasMore)
return (
<View style={styles.footerContainer}>
<Text style={styles.footerText}>{t("noMoreData")}</Text>
</View>
);
if (loadingMore)
return (
<View style={styles.footerContainer}>
<ActivityIndicator size="small" color="#0066FF" />
<Text style={styles.footerText}>{t("loadingMore")}</Text>
</View>
);
return <View style={styles.footerSpace} />;
}, [loadingMore, hasMore, t]);
// 处理滚动事件
const handleScroll = useCallback((event: any) => {
const offsetY = event.nativeEvent.contentOffset.y;
// 当滚动超过屏幕高度的一半时显示回到顶部按钮
setShowBackToTop(offsetY > 300);
}, []);
// 回到顶部
const scrollToTop = useCallback(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
}, []);
// 处理标签切换
const handleTabChange = useCallback(
(tab: "default" | "volume" | "price") => {
// 如果点击的是已经激活的价格标签,则切换排序顺序
if (tab === "price" && activeTab === "price") {
// 如果当前是价格升序,则切换为降序;如果是降序或未设置,则切换为升序
const newOrder = sortOrder === "asc" ? "desc" : "asc";
handleSort("price", newOrder);
scrollToTop();
} else {
setActiveTab(tab);
// 根据标签类型设置排序规则
if (tab === "price") {
// 默认价格从低到高
handleSort("price", "asc");
scrollToTop();
} else if (tab === "volume") {
// 按销量排序
const sortedProducts = [...originalProducts];
sortedProducts.sort((a, b) => {
const volumeA = a.sold_out || 0;
const volumeB = b.sold_out || 0;
return volumeB - volumeA; // 从高到低排序
});
setProducts(sortedProducts);
scrollToTop();
} else {
// 默认排序 - 恢复到原始数据顺序
setProducts([...originalProducts]);
scrollToTop();
}
}
},
[handleSort, activeTab, sortOrder, originalProducts, scrollToTop]
);
// 渲染骨架屏网格
const renderSkeletonGrid = useCallback(() => {
// Create an array of items for the skeleton grid
const skeletonArray = Array(8).fill(null);
return (
<View style={styles.productGrid}>
<FlatList
data={skeletonArray}
renderItem={() => <ProductSkeleton />}
keyExtractor={(_, index) => `skeleton-${index}`}
numColumns={2}
scrollEnabled={false}
contentContainerStyle={styles.productGrid}
/>
</View>
);
}, []);
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<View style={styles.safeAreaContent}>
<View style={styles.container}>
{/* 搜索栏 */}
<View style={styles.searchHeader}>
<TouchableOpacity style={styles.backButton} onPress={goBack}>
<Svg width="11" height="18" viewBox="0 0 11 18" fill="none">
<Path
d="M8.52018 17.1171L10.0867 15.6172L3.19348 8.93139L10.2127 2.37801L8.67501 0.848572L0.0893813 8.90185L8.52018 17.1171Z"
fill={"black"} // 动态修改颜色
/>
</Svg>
</TouchableOpacity>
<View style={styles.searchBar}>
<View style={{ marginRight: 8, marginLeft: 4 }}>
<SearchIcon color="#373737" size={20} />
</View>
<TextInput
style={styles.searchInput}
placeholder={t("searchProducts")}
placeholderTextColor="#999"
value={searchText}
onChangeText={setSearchText}
returnKeyType="search"
onSubmitEditing={handleSearch}
/>
{searchText.length > 0 && (
<TouchableOpacity
onPress={() => setSearchText("")}
style={styles.clearButton}
>
<IconComponent name="close-circle" size={18} color="#999" />
</TouchableOpacity>
)}
</View>
<TouchableOpacity style={styles.searchButton} onPress={handleSearch}>
<Text style={styles.searchButtonText}>{t("cancel")}</Text>
</TouchableOpacity>
</View>
{/* 标签筛选 */}
<View style={styles.tabContainer}>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === "default" && styles.activeTabButton,
]}
onPress={() => handleTabChange("default")}
>
<Text
style={[
styles.tabText,
activeTab === "default" && styles.activeTabText,
]}
>
{t('default')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === "volume" && styles.activeTabButton,
]}
onPress={() => handleTabChange("volume")}
>
<Text
style={[
styles.tabText,
activeTab === "volume" && styles.activeTabText,
]}
>
{t('volume')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.tabButton,
activeTab === "price" && styles.activeTabButton,
]}
onPress={() => handleTabChange("price")}
>
<View style={styles.tabButtonContent}>
<Text
style={[
styles.tabText,
activeTab === "price" && styles.activeTabText,
]}
>
{t('price')}
</Text>
{activeTab === "price" && (
<View style={styles.tabIcon}>
<IconComponent
name={sortOrder === "desc" ? "chevron-down" : "chevron-up"}
size={16}
color="#000"
/>
</View>
)}
</View>
</TouchableOpacity>
</View>
{/* 搜索结果 */}
<View style={styles.resultsContainer}>
{/* 搜索结果标题栏和排序选项 */}
{isFilterVisible && (
<View style={styles.resultsHeader}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.sortScrollView}
>
<View style={styles.sortGroup}>
<Text style={styles.sortLabel}>{t("price")}:</Text>
<View style={styles.sortButtons}>
<TouchableOpacity
style={[
styles.sortButton,
sortField === "price" && sortOrder === "asc"
? styles.sortButtonActive
: {},
]}
onPress={() => handleSort("price", "asc")}
>
<Text
style={[
styles.sortButtonText,
sortField === "price" && sortOrder === "asc"
? styles.sortButtonTextActive
: {},
]}
>
{t("lowToHigh")}
</Text>
{sortField === "price" && sortOrder === "asc" && (
<IconComponent
name="chevron-up"
size={16}
color="#ff6600"
/>
)}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sortButton,
sortField === "price" && sortOrder === "desc"
? styles.sortButtonActive
: {},
]}
onPress={() => handleSort("price", "desc")}
>
<Text
style={[
styles.sortButtonText,
sortField === "price" && sortOrder === "desc"
? styles.sortButtonTextActive
: {},
]}
>
{t("highToLow")}
</Text>
{sortField === "price" && sortOrder === "desc" && (
<IconComponent
name="chevron-down"
size={16}
color="#ff6600"
/>
)}
</TouchableOpacity>
</View>
</View>
<View style={styles.sortDivider} />
<View style={styles.sortGroup}>
<Text style={styles.sortLabel}>{t('time')}:</Text>
<View style={styles.sortButtons}>
<TouchableOpacity
style={[
styles.sortButton,
sortField === "time" && sortOrder === "asc"
? styles.sortButtonActive
: {},
]}
onPress={() => handleSort("time", "asc")}
>
<Text
style={[
styles.sortButtonText,
sortField === "time" && sortOrder === "asc"
? styles.sortButtonTextActive
: {},
]}
>
{t("oldest")}
</Text>
{sortField === "time" && sortOrder === "asc" && (
<IconComponent
name="chevron-up"
size={16}
color="#ff6600"
/>
)}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sortButton,
sortField === "time" && sortOrder === "desc"
? styles.sortButtonActive
: {},
]}
onPress={() => handleSort("time", "desc")}
>
<Text
style={[
styles.sortButtonText,
sortField === "time" && sortOrder === "desc"
? styles.sortButtonTextActive
: {},
]}
>
{t("newest")}
</Text>
{sortField === "time" && sortOrder === "desc" && (
<IconComponent
name="chevron-down"
size={16}
color="#ff6600"
/>
)}
</TouchableOpacity>
</View>
</View>
</ScrollView>
</View>
)}
{/* 加载指示器或产品列表 */}
{loading && showSkeleton ? (
renderSkeletonGrid()
) : (
<>
<FlatList
ref={flatListRef}
data={products}
renderItem={renderProductItem}
keyExtractor={keyExtractor}
numColumns={2}
contentContainerStyle={styles.productGrid}
ListEmptyComponent={renderEmptyList}
ListFooterComponent={renderFooter}
showsVerticalScrollIndicator={false}
initialNumToRender={4}
maxToRenderPerBatch={8}
windowSize={3}
removeClippedSubviews={Platform.OS !== "web"}
updateCellsBatchingPeriod={50}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
onScroll={handleScroll}
scrollEventThrottle={16}
/>
{showBackToTop && (
<TouchableOpacity
style={styles.backToTopButton}
onPress={scrollToTop}
activeOpacity={0.7}
>
<IconComponent name="arrow-up" size={24} color="#fff" />
</TouchableOpacity>
)}
</>
)}
</View>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
safeAreaContent: {
flex: 1,
paddingTop: Platform.OS === 'android' ? 10 : 0,
},
container: {
flex: 1,
backgroundColor: '#fff',
},
searchHeader: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
paddingVertical: 8,
backgroundColor: "#fff",
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
backButton: {
padding: 4,
},
searchBar: {
flex: 1,
flexDirection: "row",
alignItems: "center",
backgroundColor: "#f5f5f5",
borderRadius: 20,
paddingHorizontal: 8,
height: widthUtils(40, 40).height,
marginHorizontal: 8,
position: "relative",
},
searchInput: {
flex: 1,
marginLeft: 4,
fontSize: isSmallScreen ? 14 : 16,
color: "#333",
height: widthUtils(40, 40).height,
paddingRight: 32,
},
clearButton: {
position: "absolute",
right: 8,
top: "50%",
transform: [{ translateY: -9 }],
padding: 4,
},
searchButton: {
paddingVertical: 4,
paddingHorizontal: 8,
},
searchButtonText: {
fontSize: isSmallScreen ? 14 : 16,
color: "#333",
fontWeight: "500",
},
tabContainer: {
flexDirection: "row",
backgroundColor: "#fff",
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
tabButton: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingVertical: 12,
position: "relative",
},
tabButtonContent: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
tabIcon: {
marginLeft: 4,
},
tabText: {
fontSize: fontSize(16),
color: "#000",
},
activeTabText: {
color: "#0933a1",
fontWeight: "bold",
},
activeTabButton: {
// borderBottomColor: "#0933a1",
},
resultsContainer: {
flex: 1,
},
resultsHeader: {
backgroundColor: "#fff",
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
paddingVertical: 8,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
productGrid: {
padding: 8,
},
productCard: {
flex: 1,
margin: 8,
backgroundColor: "#fff",
borderRadius: 8,
overflow: "hidden",
},
productImageContainer: {
height: widthUtils(190, 190).height,
backgroundColor: "#f9f9f9",
alignItems: "center",
justifyContent: "center",
},
productImage: {
width: "100%",
height: "100%",
},
placeholderText: {
color: "#999",
fontSize: fontSize(14),
},
productInfo: {
padding: 8,
},
categoryText: {
fontSize: isSmallScreen ? 12 : 14,
color: "#000000",
fontWeight: "600",
marginBottom: 4,
fontFamily: "PingFang SC",
letterSpacing: 0,
},
priceRow: {
alignItems: "baseline",
marginBottom: 2,
},
currentPrice: {
fontSize: fontSize(24),
fontWeight: "600",
color: "#ff6600",
marginRight: 4,
},
currency: {
fontSize: fontSize(14),
fontWeight: "600",
fontFamily: "PingFang SC",
color: "#ff6600",
},
originalPrice: {
fontSize: fontSize(14),
color: "#999",
textDecorationLine: "line-through",
},
currencySmall: {
fontSize: fontSize(14),
color: "#9a9a9a",
fontWeight: "600",
fontFamily: "PingFang SC",
},
productSales: {
fontSize: fontSize(14),
fontWeight: "600",
fontFamily: "PingFang SC",
color: "#7c7c7c",
},
sortScrollView: {
flexGrow: 0,
},
sortGroup: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
},
sortLabel: {
fontSize: fontSize(16),
color: "#666",
marginRight: 8,
},
sortButtons: {
flexDirection: "row",
alignItems: "center",
},
sortButton: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 4,
paddingHorizontal: 8,
borderRadius: 4,
marginLeft: 4,
borderWidth: 1,
borderColor: "#e0e0e0",
},
sortButtonActive: {
borderColor: "#ff6600",
backgroundColor: "#fff8f5",
},
sortButtonText: {
fontSize: fontSize(14),
color: "#666",
},
sortButtonTextActive: {
color: "#ff6600",
fontWeight: "bold",
},
sortDivider: {
width: 1,
height: widthUtils(20, 20).height,
backgroundColor: "#e0e0e0",
marginHorizontal: 16,
},
footerContainer: {
padding: 16,
alignItems: "center",
flexDirection: "row",
justifyContent: "center",
},
footerText: {
fontSize: fontSize(14),
color: "#666",
marginLeft: 8,
},
footerSpace: {
height: widthUtils(20, 20).height,
},
backToTopButton: {
position: "absolute",
bottom: 20,
right: 20,
width: widthUtils(20, 20).width,
height: widthUtils(20, 20).height,
borderRadius: 22,
backgroundColor: "#0066FF",
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
emptyContainer: {
flex: 1,
minHeight: 300,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
emptyText: {
fontSize: fontSize(14),
fontWeight: "bold",
color: "#333",
marginTop: 16,
marginBottom: 8,
},
emptySubtext: {
fontSize: fontSize(14),
color: "#999",
textAlign: "center",
},
resultsTitle: {
fontSize: fontSize(14),
fontWeight: "bold",
color: "#333",
flex: 1,
},
resultsCount: {
fontSize: fontSize(14),
color: "#999",
},
filterButton: {
marginLeft: 8,
padding: 4,
},
imagePlaceholder: {
backgroundColor: '#EAEAEA',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
},
vipIcon: {
position: "absolute",
top: 0,
right: 0,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#3b3b3b",
borderRadius: 10,
flexDirection: "row",
width: widthUtils(30, 66).width,
height: widthUtils(30, 66).height,
},
vipButtonText: {
fontStyle: "italic",
fontWeight: "900",
fontSize: fontSize(18),
color: "#f1c355",
},
vipLabelBold: {
fontStyle: "italic",
fontWeight: "900",
fontSize: fontSize(18),
color: "#f1c355",
},
beautyProductInfoRow: {
flexDirection: "row",
alignItems: "center",
},
flexRowCentered: {},
priceContainer: {
flexDirection: "row",
},
highlightedText: {
fontWeight: "700",
fontSize: fontSize(24),
color: "#ff5100",
},
highlightedText1: {
fontWeight: "700",
fontSize: fontSize(14),
color: "#ff5100",
},
priceContainer1: {},
priceLabel1: {
fontSize: fontSize(12),
fontWeight: "600",
color: "#9a9a9a",
textDecorationLine: "line-through",
},
skeletonText: {
backgroundColor: '#EAEAEA',
borderRadius: 4,
},
});