444 lines
11 KiB
TypeScript
444 lines
11 KiB
TypeScript
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; |