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

View File

@@ -10,10 +10,12 @@ import {
Text,
TouchableOpacity,
View,
Alert,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useCart } from '../../../hooks/useCart';
import { useProduct } from '../../../hooks/useProducts';
import { wishlistApi } from '../../../services/wishlistApi ';
const { width } = Dimensions.get('window');
@@ -25,6 +27,8 @@ export default function ProductDetailScreen() {
const { product, loading, error } = useProduct(productId);
const { addToCart } = useCart();
const [isAddingToCart, setIsAddingToCart] = useState(false);
const [isInWishlist, setIsInWishlist] = useState(false);
const [isLoadingWishlist, setIsLoadingWishlist] = useState(false);
const formatPrice = (price: number) => {
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) {
return (
<SafeAreaView style={styles.container}>
@@ -90,16 +123,37 @@ export default function ProductDetailScreen() {
<Text style={styles.headerTitle} numberOfLines={1}>
Chi tiết sản phẩm
</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>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
{/* Product Image */}
<Image
source={{ uri: product.imageUrl || 'https://via.placeholder.com/400' }}
style={styles.productImage}
resizeMode="cover"
/>
<View style={styles.imageContainer}>
<Image
source={{ uri: product.imageUrl || 'https://via.placeholder.com/400' }}
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 */}
<View style={styles.content}>
@@ -182,6 +236,26 @@ export default function ProductDetailScreen() {
{/* Bottom Action Buttons */}
<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
style={[
styles.actionButton,
@@ -249,14 +323,41 @@ const styles = StyleSheet.create({
color: '#333',
textAlign: 'center',
},
wishlistHeaderBtn: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
scrollView: {
flex: 1,
},
productImage: {
imageContainer: {
position: 'relative',
width: width,
height: width,
},
productImage: {
width: '100%',
height: '100%',
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: {
padding: 16,
},
@@ -308,9 +409,6 @@ const styles = StyleSheet.create({
outOfStockBadge: {
backgroundColor: '#ffebee',
},
outOfStockText: {
color: '#f44336',
},
divider: {
height: 1,
backgroundColor: '#e0e0e0',
@@ -369,6 +467,15 @@ const styles = StyleSheet.create({
borderRadius: 12,
gap: 8,
},
wishlistActionButton: {
flex: 0.5,
backgroundColor: '#fff',
borderWidth: 2,
borderColor: '#ff6b6b',
},
wishlistActionButtonActive: {
backgroundColor: '#ff6b6b',
},
addToCartButton: {
backgroundColor: '#ff9800',
},

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;

View File

@@ -1,28 +1,32 @@
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 { ProductResponse } from '../services/product';
import { useRouter } from 'expo-router';
import { useCart } from '../hooks/useCart';
import { wishlistApi } from '../services/wishlistApi ';
interface ProductCardProps {
product: ProductResponse;
onWishlistToggle?: (isAdded: boolean) => void;
}
const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
const ProductCard: React.FC<ProductCardProps> = ({ product, onWishlistToggle }) => {
const router = useRouter();
const { addToCart } = useCart();
const [isAdding, setIsAdding] = useState(false);
const [isInWishlist, setIsInWishlist] = useState(false);
const [isLoadingWishlist, setIsLoadingWishlist] = useState(false);
const handlePress = () => {
router.push(`/products/${product.productId}`);
};
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) {
return; // Không cho thêm vào giỏ nếu hết hàng
return;
}
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) => {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
@@ -54,6 +89,26 @@ const ProductCard: React.FC<ProductCardProps> = ({ product }) => {
<Text style={styles.categoryText}>{product.categoryName}</Text>
</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 */}
{product.stockQuantity <= 0 && (
<View style={styles.outOfStockOverlay}>
@@ -138,6 +193,20 @@ const styles = StyleSheet.create({
color: '#fff',
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: {
position: 'absolute',
top: 0,

View 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
View 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
View 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();