This commit is contained in:
2025-11-19 14:34:05 +07:00
parent 08b352f686
commit 3cd2a53a0e
6 changed files with 1107 additions and 14 deletions

444
app/(tabs)/wishlist.tsx Normal file
View File

@@ -0,0 +1,444 @@
import React, { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
Image,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
RefreshControl,
Alert,
} from 'react-native';
import { AntDesign, MaterialCommunityIcons } from '@expo/vector-icons';
import { useWishlist } from '../../hooks/useWishlist';
const COLORS = {
primary: '#FF6B6B',
secondary: '#4ECDC4',
background: '#F7F9FC',
white: '#FFFFFF',
text: '#2C3E50',
lightText: '#95A5A6',
border: '#E0E0E0',
success: '#27AE60',
};
interface WishlistItem {
id?: number;
wishlistId?: number;
productId: number;
productName: string;
productImage: string;
price: number;
description: string;
addedAt: string;
}
const WishlistScreen = () => {
const {
wishlists,
loading,
error,
totalElements,
currentPage,
totalPages,
fetchWishlists,
removeFromWishlist,
clearError,
} = useWishlist();
const [refreshing, setRefreshing] = useState(false);
const [currentSort, setCurrentSort] = useState<'createdAt' | 'price' | 'name'>('createdAt');
useEffect(() => {
fetchWishlists(0, 10, currentSort);
}, [currentSort, fetchWishlists]);
useEffect(() => {
if (error) {
Alert.alert('Lỗi', error, [{ text: 'OK', onPress: clearError }]);
}
}, [error, clearError]);
const handleRefresh = async () => {
setRefreshing(true);
await fetchWishlists(currentPage, 10, currentSort);
setRefreshing(false);
};
const handleRemoveFromWishlist = (item: WishlistItem) => {
Alert.alert(
'Xóa khỏi ưu thích',
`Bạn có chắc chắn muốn xóa "${item.productName}" khỏi danh sách ưu thích?`,
[
{ text: 'Hủy', onPress: () => {}, style: 'cancel' },
{
text: 'Xóa',
onPress: async () => {
const result = await removeFromWishlist(item.productId);
if (result.success) {
Alert.alert('Thành công', result.message);
} else {
Alert.alert('Lỗi', result.message);
}
},
style: 'destructive',
},
]
);
};
const handleLoadMore = () => {
if (currentPage < totalPages - 1 && !loading) {
fetchWishlists(currentPage + 1, 10, currentSort);
}
};
const formatPrice = (price: number) => {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
}).format(price);
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('vi-VN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
const renderWishlistItem = ({ item }: { item: WishlistItem | any }) => {
// Kiểm tra dữ liệu hợp lệ
if (!item || !item.productId) {
return null;
}
return (
<View style={styles.itemContainer}>
<Image
source={{ uri: item.productImage || 'https://via.placeholder.com/120' }}
style={styles.itemImage}
/>
<View style={styles.itemContent}>
<Text style={styles.itemName} numberOfLines={2}>
{item.productName}
</Text>
<Text style={styles.itemDescription} numberOfLines={2}>
{item.description}
</Text>
<View style={styles.priceContainer}>
<Text style={styles.price}>{formatPrice(item.price)}</Text>
<Text style={styles.addedDate}>{formatDate(item.addedAt)}</Text>
</View>
</View>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => handleRemoveFromWishlist(item)}
>
<AntDesign name="delete" size={20} color={COLORS.primary} />
</TouchableOpacity>
</View>
);
};
const renderEmptyState = () => (
<View style={styles.emptyContainer}>
<MaterialCommunityIcons
name="heart-outline"
size={80}
color={COLORS.lightText}
/>
<Text style={styles.emptyText}>Danh sách ưu thích trống</Text>
<Text style={styles.emptySubText}>
Thêm những sản phẩm yêu thích của bạn vào đây
</Text>
</View>
);
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Danh sách ưu thích</Text>
<View style={styles.countBadge}>
<Text style={styles.countText}>{totalElements}</Text>
</View>
</View>
{/* Sort Options */}
<View style={styles.sortContainer}>
<TouchableOpacity
style={[
styles.sortButton,
currentSort === 'createdAt' && styles.sortButtonActive,
]}
onPress={() => setCurrentSort('createdAt')}
>
<Text
style={[
styles.sortButtonText,
currentSort === 'createdAt' && styles.sortButtonTextActive,
]}
>
Mới nhất
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sortButton,
currentSort === 'price' && styles.sortButtonActive,
]}
onPress={() => setCurrentSort('price')}
>
<Text
style={[
styles.sortButtonText,
currentSort === 'price' && styles.sortButtonTextActive,
]}
>
Giá
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.sortButton,
currentSort === 'name' && styles.sortButtonActive,
]}
onPress={() => setCurrentSort('name')}
>
<Text
style={[
styles.sortButtonText,
currentSort === 'name' && styles.sortButtonTextActive,
]}
>
Tên
</Text>
</TouchableOpacity>
</View>
{/* Content */}
{loading && wishlists.length === 0 ? (
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color={COLORS.primary} />
<Text style={styles.loadingText}>Đang tải...</Text>
</View>
) : (
<FlatList
data={wishlists}
renderItem={renderWishlistItem}
keyExtractor={(item, index) => {
// Use only item.id with fallback to index to ensure proper typing
return item?.id?.toString() ?? index.toString();
}}
contentContainerStyle={styles.listContent}
ListEmptyComponent={renderEmptyState()}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor={COLORS.primary}
/>
}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={
loading && wishlists.length > 0 ? (
<View style={styles.footerLoader}>
<ActivityIndicator size="small" color={COLORS.primary} />
</View>
) : null
}
/>
)}
{/* Pagination Info */}
{totalElements > 0 && (
<View style={styles.paginationInfo}>
<Text style={styles.paginationText}>
Trang {currentPage + 1}/{totalPages}
</Text>
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 16,
backgroundColor: COLORS.white,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
headerTitle: {
fontSize: 24,
fontWeight: '700',
color: COLORS.text,
},
countBadge: {
backgroundColor: COLORS.primary,
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 6,
},
countText: {
color: COLORS.white,
fontWeight: '600',
fontSize: 12,
},
sortContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: COLORS.white,
gap: 8,
},
sortButton: {
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
borderColor: COLORS.border,
backgroundColor: COLORS.white,
},
sortButtonActive: {
backgroundColor: COLORS.primary,
borderColor: COLORS.primary,
},
sortButtonText: {
fontSize: 12,
fontWeight: '600',
color: COLORS.text,
},
sortButtonTextActive: {
color: COLORS.white,
},
listContent: {
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 20,
},
itemContainer: {
flexDirection: 'row',
backgroundColor: COLORS.white,
borderRadius: 12,
marginBottom: 12,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
itemImage: {
width: 100,
height: 120,
backgroundColor: COLORS.background,
},
itemContent: {
flex: 1,
paddingHorizontal: 12,
paddingVertical: 10,
justifyContent: 'space-between',
},
itemName: {
fontSize: 14,
fontWeight: '700',
color: COLORS.text,
marginBottom: 4,
},
itemDescription: {
fontSize: 12,
color: COLORS.lightText,
marginBottom: 8,
},
priceContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
price: {
fontSize: 14,
fontWeight: '700',
color: COLORS.primary,
},
addedDate: {
fontSize: 10,
color: COLORS.lightText,
},
deleteButton: {
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
},
centerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: COLORS.lightText,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 32,
},
emptyText: {
fontSize: 18,
fontWeight: '700',
color: COLORS.text,
marginTop: 16,
},
emptySubText: {
fontSize: 14,
color: COLORS.lightText,
marginTop: 8,
textAlign: 'center',
},
footerLoader: {
paddingVertical: 16,
justifyContent: 'center',
alignItems: 'center',
},
paginationInfo: {
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: COLORS.white,
borderTopWidth: 1,
borderTopColor: COLORS.border,
alignItems: 'center',
},
paginationText: {
fontSize: 12,
color: COLORS.lightText,
fontWeight: '600',
},
});
export default WishlistScreen;