wishlist
This commit is contained in:
@@ -10,10 +10,12 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useCart } from '../../../hooks/useCart';
|
import { useCart } from '../../../hooks/useCart';
|
||||||
import { useProduct } from '../../../hooks/useProducts';
|
import { useProduct } from '../../../hooks/useProducts';
|
||||||
|
import { wishlistApi } from '../../../services/wishlistApi ';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ export default function ProductDetailScreen() {
|
|||||||
const { product, loading, error } = useProduct(productId);
|
const { product, loading, error } = useProduct(productId);
|
||||||
const { addToCart } = useCart();
|
const { addToCart } = useCart();
|
||||||
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
||||||
|
const [isInWishlist, setIsInWishlist] = useState(false);
|
||||||
|
const [isLoadingWishlist, setIsLoadingWishlist] = useState(false);
|
||||||
|
|
||||||
const formatPrice = (price: number) => {
|
const formatPrice = (price: number) => {
|
||||||
return new Intl.NumberFormat('vi-VN', {
|
return new Intl.NumberFormat('vi-VN', {
|
||||||
@@ -54,6 +58,35 @@ export default function ProductDetailScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleWishlist = async () => {
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
setIsLoadingWishlist(true);
|
||||||
|
try {
|
||||||
|
if (isInWishlist) {
|
||||||
|
const response = await wishlistApi.removeFromWishlist(product.productId);
|
||||||
|
if (response.success) {
|
||||||
|
setIsInWishlist(false);
|
||||||
|
Alert.alert('Thành công', response.message);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Lỗi', response.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const response = await wishlistApi.addToWishlist(product.productId);
|
||||||
|
if (response.success) {
|
||||||
|
setIsInWishlist(true);
|
||||||
|
Alert.alert('Thành công', response.message);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Lỗi', response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Lỗi', error instanceof Error ? error.message : 'Lỗi không xác định');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingWishlist(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
@@ -90,16 +123,37 @@ export default function ProductDetailScreen() {
|
|||||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||||
Chi tiết sản phẩm
|
Chi tiết sản phẩm
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ width: 40 }} />
|
<TouchableOpacity
|
||||||
|
style={styles.wishlistHeaderBtn}
|
||||||
|
onPress={handleToggleWishlist}
|
||||||
|
disabled={isLoadingWishlist}
|
||||||
|
>
|
||||||
|
{isLoadingWishlist ? (
|
||||||
|
<ActivityIndicator size="small" color="#ff6b6b" />
|
||||||
|
) : (
|
||||||
|
<Ionicons
|
||||||
|
name={isInWishlist ? 'heart' : 'heart-outline'}
|
||||||
|
size={24}
|
||||||
|
color={isInWishlist ? '#ff6b6b' : '#333'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
||||||
{/* Product Image */}
|
{/* Product Image */}
|
||||||
<Image
|
<View style={styles.imageContainer}>
|
||||||
source={{ uri: product.imageUrl || 'https://via.placeholder.com/400' }}
|
<Image
|
||||||
style={styles.productImage}
|
source={{ uri: product.imageUrl || 'https://via.placeholder.com/400' }}
|
||||||
resizeMode="cover"
|
style={styles.productImage}
|
||||||
/>
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
{product.stockQuantity <= 0 && (
|
||||||
|
<View style={styles.outOfStockOverlay}>
|
||||||
|
<Text style={styles.outOfStockText}>HẾT HÀNG</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Product Info */}
|
{/* Product Info */}
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
@@ -182,6 +236,26 @@ export default function ProductDetailScreen() {
|
|||||||
|
|
||||||
{/* Bottom Action Buttons */}
|
{/* Bottom Action Buttons */}
|
||||||
<View style={styles.bottomActions}>
|
<View style={styles.bottomActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.actionButton,
|
||||||
|
styles.wishlistActionButton,
|
||||||
|
isInWishlist && styles.wishlistActionButtonActive,
|
||||||
|
]}
|
||||||
|
onPress={handleToggleWishlist}
|
||||||
|
disabled={isLoadingWishlist}
|
||||||
|
>
|
||||||
|
{isLoadingWishlist ? (
|
||||||
|
<ActivityIndicator size="small" color={isInWishlist ? '#fff' : '#ff6b6b'} />
|
||||||
|
) : (
|
||||||
|
<Ionicons
|
||||||
|
name={isInWishlist ? 'heart' : 'heart-outline'}
|
||||||
|
size={24}
|
||||||
|
color={isInWishlist ? '#fff' : '#ff6b6b'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
@@ -249,14 +323,41 @@ const styles = StyleSheet.create({
|
|||||||
color: '#333',
|
color: '#333',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
wishlistHeaderBtn: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
scrollView: {
|
scrollView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
productImage: {
|
imageContainer: {
|
||||||
|
position: 'relative',
|
||||||
width: width,
|
width: width,
|
||||||
height: width,
|
height: width,
|
||||||
|
},
|
||||||
|
productImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
backgroundColor: '#f0f0f0',
|
backgroundColor: '#f0f0f0',
|
||||||
},
|
},
|
||||||
|
outOfStockOverlay: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
outOfStockText: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#fff',
|
||||||
|
letterSpacing: 2,
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
@@ -308,9 +409,6 @@ const styles = StyleSheet.create({
|
|||||||
outOfStockBadge: {
|
outOfStockBadge: {
|
||||||
backgroundColor: '#ffebee',
|
backgroundColor: '#ffebee',
|
||||||
},
|
},
|
||||||
outOfStockText: {
|
|
||||||
color: '#f44336',
|
|
||||||
},
|
|
||||||
divider: {
|
divider: {
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: '#e0e0e0',
|
backgroundColor: '#e0e0e0',
|
||||||
@@ -369,6 +467,15 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
|
wishlistActionButton: {
|
||||||
|
flex: 0.5,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#ff6b6b',
|
||||||
|
},
|
||||||
|
wishlistActionButtonActive: {
|
||||||
|
backgroundColor: '#ff6b6b',
|
||||||
|
},
|
||||||
addToCartButton: {
|
addToCartButton: {
|
||||||
backgroundColor: '#ff9800',
|
backgroundColor: '#ff9800',
|
||||||
},
|
},
|
||||||
|
|||||||
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;
|
||||||
@@ -1,28 +1,32 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Image, ActivityIndicator } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, Image, ActivityIndicator, Alert } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { ProductResponse } from '../services/product';
|
import { ProductResponse } from '../services/product';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useCart } from '../hooks/useCart';
|
import { useCart } from '../hooks/useCart';
|
||||||
|
import { wishlistApi } from '../services/wishlistApi ';
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
product: ProductResponse;
|
product: ProductResponse;
|
||||||
|
onWishlistToggle?: (isAdded: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
|
const ProductCard: React.FC<ProductCardProps> = ({ product, onWishlistToggle }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { addToCart } = useCart();
|
const { addToCart } = useCart();
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [isInWishlist, setIsInWishlist] = useState(false);
|
||||||
|
const [isLoadingWishlist, setIsLoadingWishlist] = useState(false);
|
||||||
|
|
||||||
const handlePress = () => {
|
const handlePress = () => {
|
||||||
router.push(`/products/${product.productId}`);
|
router.push(`/products/${product.productId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCart = async (e: any) => {
|
const handleAddToCart = async (e: any) => {
|
||||||
e.stopPropagation(); // Ngăn chặn việc navigate khi click vào nút giỏ hàng
|
e.stopPropagation();
|
||||||
|
|
||||||
if (product.stockQuantity <= 0) {
|
if (product.stockQuantity <= 0) {
|
||||||
return; // Không cho thêm vào giỏ nếu hết hàng
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsAdding(true);
|
setIsAdding(true);
|
||||||
@@ -33,6 +37,37 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleWishlist = async (e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsLoadingWishlist(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isInWishlist) {
|
||||||
|
const response = await wishlistApi.removeFromWishlist(product.productId);
|
||||||
|
if (response.success) {
|
||||||
|
setIsInWishlist(false);
|
||||||
|
onWishlistToggle?.(false);
|
||||||
|
Alert.alert('Thành công', response.message);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Lỗi', response.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const response = await wishlistApi.addToWishlist(product.productId);
|
||||||
|
if (response.success) {
|
||||||
|
setIsInWishlist(true);
|
||||||
|
onWishlistToggle?.(true);
|
||||||
|
Alert.alert('Thành công', response.message);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Lỗi', response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Lỗi', error instanceof Error ? error.message : 'Lỗi không xác định');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingWishlist(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatPrice = (price: number) => {
|
const formatPrice = (price: number) => {
|
||||||
return new Intl.NumberFormat('vi-VN', {
|
return new Intl.NumberFormat('vi-VN', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -54,6 +89,26 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
|
|||||||
<Text style={styles.categoryText}>{product.categoryName}</Text>
|
<Text style={styles.categoryText}>{product.categoryName}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Wishlist Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.wishlistButton,
|
||||||
|
isInWishlist && styles.wishlistButtonActive,
|
||||||
|
]}
|
||||||
|
onPress={handleToggleWishlist}
|
||||||
|
disabled={isLoadingWishlist}
|
||||||
|
>
|
||||||
|
{isLoadingWishlist ? (
|
||||||
|
<ActivityIndicator size="small" color={isInWishlist ? '#ff6b6b' : '#fff'} />
|
||||||
|
) : (
|
||||||
|
<Ionicons
|
||||||
|
name={isInWishlist ? 'heart' : 'heart-outline'}
|
||||||
|
size={20}
|
||||||
|
color={isInWishlist ? '#ff6b6b' : '#fff'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Out of Stock Overlay */}
|
{/* Out of Stock Overlay */}
|
||||||
{product.stockQuantity <= 0 && (
|
{product.stockQuantity <= 0 && (
|
||||||
<View style={styles.outOfStockOverlay}>
|
<View style={styles.outOfStockOverlay}>
|
||||||
@@ -138,6 +193,20 @@ const styles = StyleSheet.create({
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
wishlistButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 12,
|
||||||
|
right: 12,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
wishlistButtonActive: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
},
|
||||||
outOfStockOverlay: {
|
outOfStockOverlay: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
239
components/WishlistButton.tsx
Normal file
239
components/WishlistButton.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from 'react-native';
|
||||||
|
import { AntDesign } from '@expo/vector-icons';
|
||||||
|
import { wishlistApi } from '../services/wishlistApi ';
|
||||||
|
|
||||||
|
interface WishlistButtonProps {
|
||||||
|
productId: number;
|
||||||
|
onToggle?: (isAdded: boolean) => void;
|
||||||
|
style?: any;
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
|
showLabel?: boolean;
|
||||||
|
isInWishlist?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLORS = {
|
||||||
|
primary: '#FF6B6B',
|
||||||
|
secondary: '#4ECDC4',
|
||||||
|
white: '#FFFFFF',
|
||||||
|
text: '#2C3E50',
|
||||||
|
lightText: '#95A5A6',
|
||||||
|
border: '#E0E0E0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const WishlistButton: React.FC<WishlistButtonProps> = ({
|
||||||
|
productId,
|
||||||
|
onToggle,
|
||||||
|
style,
|
||||||
|
size = 'medium',
|
||||||
|
showLabel = false,
|
||||||
|
isInWishlist: initialIsInWishlist = false,
|
||||||
|
}) => {
|
||||||
|
const [isInWishlist, setIsInWishlist] = useState(initialIsInWishlist);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const getSizeStyles = () => {
|
||||||
|
switch (size) {
|
||||||
|
case 'small':
|
||||||
|
return {
|
||||||
|
buttonSize: 32,
|
||||||
|
iconSize: 16,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
};
|
||||||
|
case 'large':
|
||||||
|
return {
|
||||||
|
buttonSize: 56,
|
||||||
|
iconSize: 28,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
};
|
||||||
|
default: // medium
|
||||||
|
return {
|
||||||
|
buttonSize: 44,
|
||||||
|
iconSize: 20,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeStyles = getSizeStyles();
|
||||||
|
|
||||||
|
const handleToggleWishlist = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
if (isInWishlist) {
|
||||||
|
// Remove from wishlist
|
||||||
|
const response = await wishlistApi.removeFromWishlist(productId);
|
||||||
|
if (response.success) {
|
||||||
|
setIsInWishlist(false);
|
||||||
|
Alert.alert('Thành công', response.message);
|
||||||
|
onToggle?.(false);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Lỗi', response.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add to wishlist
|
||||||
|
const response = await wishlistApi.addToWishlist(productId);
|
||||||
|
if (response.success) {
|
||||||
|
setIsInWishlist(true);
|
||||||
|
Alert.alert('Thành công', response.message);
|
||||||
|
onToggle?.(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert('Lỗi', response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert(
|
||||||
|
'Lỗi',
|
||||||
|
error instanceof Error ? error.message : 'Lỗi không xác định'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (size === 'large' && showLabel) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.largeButtonContainer, style]}
|
||||||
|
onPress={handleToggleWishlist}
|
||||||
|
disabled={loading}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.largeButton,
|
||||||
|
isInWishlist && styles.largeButtonActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator color={isInWishlist ? COLORS.white : COLORS.primary} />
|
||||||
|
) : (
|
||||||
|
<AntDesign
|
||||||
|
name={isInWishlist ? ('heart' as any) : ('hearto' as any)}
|
||||||
|
size={sizeStyles.iconSize}
|
||||||
|
color={isInWishlist ? COLORS.white : COLORS.primary}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.largeButtonLabel,
|
||||||
|
isInWishlist && styles.largeButtonLabelActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{isInWishlist ? 'Đã yêu thích' : 'Yêu thích'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
{
|
||||||
|
width: sizeStyles.buttonSize,
|
||||||
|
height: sizeStyles.buttonSize,
|
||||||
|
borderRadius: sizeStyles.buttonSize / 2,
|
||||||
|
paddingHorizontal: sizeStyles.paddingHorizontal,
|
||||||
|
paddingVertical: sizeStyles.paddingVertical,
|
||||||
|
},
|
||||||
|
isInWishlist && styles.buttonActive,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
onPress={handleToggleWishlist}
|
||||||
|
disabled={loading}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<ActivityIndicator
|
||||||
|
color={isInWishlist ? COLORS.white : COLORS.primary}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AntDesign
|
||||||
|
name={isInWishlist ? ('heart' as any) : ('hearto' as any)}
|
||||||
|
size={sizeStyles.iconSize}
|
||||||
|
color={isInWishlist ? COLORS.white : COLORS.primary}
|
||||||
|
/>
|
||||||
|
{showLabel && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
{ fontSize: sizeStyles.fontSize },
|
||||||
|
isInWishlist ? styles.labelActive : styles.label,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{isInWishlist ? 'Đã thích' : 'Thích'}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
button: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: COLORS.white,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: COLORS.primary,
|
||||||
|
},
|
||||||
|
buttonActive: {
|
||||||
|
backgroundColor: COLORS.primary,
|
||||||
|
borderColor: COLORS.primary,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: COLORS.primary,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
labelActive: {
|
||||||
|
color: COLORS.white,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
largeButtonContainer: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
largeButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: COLORS.white,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: COLORS.primary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
largeButtonActive: {
|
||||||
|
backgroundColor: COLORS.primary,
|
||||||
|
borderColor: COLORS.primary,
|
||||||
|
},
|
||||||
|
largeButtonLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: COLORS.primary,
|
||||||
|
},
|
||||||
|
largeButtonLabelActive: {
|
||||||
|
color: COLORS.white,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default WishlistButton;
|
||||||
150
hooks/useWishlist.ts
Normal file
150
hooks/useWishlist.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { wishlistApi, WishlistProduct } from '../services/wishlistApi ';
|
||||||
|
|
||||||
|
interface UseWishlistState {
|
||||||
|
wishlists: WishlistProduct[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
totalElements: number;
|
||||||
|
totalPages: number;
|
||||||
|
currentPage: number;
|
||||||
|
wishlistCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWishlist = () => {
|
||||||
|
const [state, setState] = useState<UseWishlistState>({
|
||||||
|
wishlists: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
totalElements: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
currentPage: 0,
|
||||||
|
wishlistCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách ưu thích
|
||||||
|
*/
|
||||||
|
const fetchWishlists = useCallback(
|
||||||
|
async (page: number = 0, size: number = 10, sortBy: string = 'createdAt') => {
|
||||||
|
setState((prev) => ({ ...prev, loading: true, error: null }));
|
||||||
|
try {
|
||||||
|
const response = await wishlistApi.getWishlists(page, size, sortBy);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// Map API response đến format chính xác
|
||||||
|
const wishlists = (response.data.wishlists || []).map((item: any) => ({
|
||||||
|
id: item.wishlistId || item.id,
|
||||||
|
wishlistId: item.wishlistId || item.id,
|
||||||
|
productId: item.productId,
|
||||||
|
productName: item.productName,
|
||||||
|
productImage: item.productImage,
|
||||||
|
price: item.price,
|
||||||
|
description: item.description,
|
||||||
|
addedAt: item.addedAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
wishlists: response.data?.wishlists || [],
|
||||||
|
totalElements: response.data?.totalElements ?? 0,
|
||||||
|
totalPages: response.data?.totalPages ?? 0,
|
||||||
|
currentPage: response.data?.currentPage ?? 0,
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Không thể lấy danh sách ưu thích');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: error instanceof Error ? error.message : 'Lỗi không xác định',
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thêm sản phẩm vào ưu thích
|
||||||
|
*/
|
||||||
|
const addToWishlist = useCallback(async (productId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await wishlistApi.addToWishlist(productId);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
wishlists: [response.data!, ...prev.wishlists],
|
||||||
|
totalElements: prev.totalElements + 1,
|
||||||
|
wishlistCount: prev.wishlistCount + 1,
|
||||||
|
}));
|
||||||
|
return { success: true, message: response.message };
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Không thể thêm vào ưu thích');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Lỗi không xác định';
|
||||||
|
setState((prev) => ({ ...prev, error: errorMessage }));
|
||||||
|
return { success: false, message: errorMessage };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xóa sản phẩm khỏi ưu thích
|
||||||
|
*/
|
||||||
|
const removeFromWishlist = useCallback(async (productId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await wishlistApi.removeFromWishlist(productId);
|
||||||
|
if (response.success) {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
wishlists: prev.wishlists.filter((w) => w.productId !== productId),
|
||||||
|
totalElements: Math.max(0, prev.totalElements - 1),
|
||||||
|
wishlistCount: Math.max(0, prev.wishlistCount - 1),
|
||||||
|
}));
|
||||||
|
return { success: true, message: response.message };
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Không thể xóa khỏi ưu thích');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Lỗi không xác định';
|
||||||
|
setState((prev) => ({ ...prev, error: errorMessage }));
|
||||||
|
return { success: false, message: errorMessage };
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy số lượng ưu thích
|
||||||
|
*/
|
||||||
|
const fetchWishlistCount = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await wishlistApi.getWishlistCount();
|
||||||
|
if (response.success && response.data !== undefined) {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
wishlistCount: response.data!,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching wishlist count:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xóa lỗi
|
||||||
|
*/
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setState((prev) => ({ ...prev, error: null }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
fetchWishlists,
|
||||||
|
addToWishlist,
|
||||||
|
removeFromWishlist,
|
||||||
|
fetchWishlistCount,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
};
|
||||||
84
services/wishlistApi .ts
Normal file
84
services/wishlistApi .ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import api from './api';
|
||||||
|
|
||||||
|
export interface WishlistProduct {
|
||||||
|
id: number;
|
||||||
|
productId: number;
|
||||||
|
productName: string;
|
||||||
|
productImage: string;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
addedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WishlistResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: WishlistProduct;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WishlistListResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: {
|
||||||
|
wishlists: WishlistProduct[];
|
||||||
|
totalPages: number;
|
||||||
|
totalElements: number;
|
||||||
|
currentPage: number;
|
||||||
|
pageSize: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WishlistCountResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WishlistApi {
|
||||||
|
/**
|
||||||
|
* Thêm sản phẩm vào danh sách ưu thích
|
||||||
|
*/
|
||||||
|
async addToWishlist(productId: number): Promise<WishlistResponse> {
|
||||||
|
return api.post<WishlistResponse>(
|
||||||
|
'/v1/wishlists/add',
|
||||||
|
{ productId },
|
||||||
|
{ requireAuth: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Xóa sản phẩm khỏi danh sách ưu thích
|
||||||
|
*/
|
||||||
|
async removeFromWishlist(productId: number): Promise<WishlistResponse> {
|
||||||
|
return api.delete<WishlistResponse>(
|
||||||
|
`/v1/wishlists/remove/${productId}`,
|
||||||
|
{ requireAuth: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách sản phẩm ưu thích
|
||||||
|
*/
|
||||||
|
async getWishlists(
|
||||||
|
page: number = 0,
|
||||||
|
size: number = 10,
|
||||||
|
sortBy: string = 'createdAt'
|
||||||
|
): Promise<WishlistListResponse> {
|
||||||
|
return api.get<WishlistListResponse>(
|
||||||
|
`/v1/wishlists?page=${page}&size=${size}&sortBy=${sortBy}`,
|
||||||
|
{ requireAuth: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy số lượng sản phẩm ưu thích
|
||||||
|
*/
|
||||||
|
async getWishlistCount(): Promise<WishlistCountResponse> {
|
||||||
|
return api.get<WishlistCountResponse>(
|
||||||
|
'/v1/wishlists/count',
|
||||||
|
{ requireAuth: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wishlistApi = new WishlistApi();
|
||||||
Reference in New Issue
Block a user