wishlist
This commit is contained in:
444
app/(tabs)/wishlist.tsx
Normal file
444
app/(tabs)/wishlist.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user