This commit is contained in:
2025-11-14 14:44:46 +07:00
parent 0ca7536b70
commit f150a84cf0
10 changed files with 1581 additions and 184 deletions

253
app/(tabs)/cart.tsx Normal file
View File

@@ -0,0 +1,253 @@
import React from 'react';
import {
View,
Text,
FlatList,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
Alert,
RefreshControl,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useFocusEffect } from '@react-navigation/native';
import { useCart } from '../../hooks/useCart';
import CartItemCard from '../../components/CartItemCard';
export default function CartScreen() {
const {
cart,
loading,
refreshing,
refreshCart,
updateQuantity,
removeItem,
clearCart,
} = useCart();
// Tự động refresh cart mỗi khi vào trang
useFocusEffect(
React.useCallback(() => {
console.log('=== CartScreen focused - Refreshing cart ===');
refreshCart();
}, [refreshCart])
);
const handleClearCart = () => {
Alert.alert(
'Xác nhận',
'Bạn có chắc muốn xóa tất cả sản phẩm trong giỏ hàng?',
[
{ text: 'Hủy', style: 'cancel' },
{
text: 'Xóa tất cả',
onPress: clearCart,
style: 'destructive'
},
]
);
};
const handleCheckout = () => {
Alert.alert('Thông báo', 'Chức năng thanh toán đang được phát triển');
};
const formatPrice = (price: number) => {
return new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
}).format(price);
};
if (loading && !cart) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#ff6b6b" />
<Text style={styles.loadingText}>Đang tải giỏ hàng...</Text>
</View>
</SafeAreaView>
);
}
if (!cart || cart.items.length === 0) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Giỏ hàng</Text>
</View>
<View style={styles.emptyContainer}>
<Ionicons name="cart-outline" size={100} color="#ccc" />
<Text style={styles.emptyText}>Giỏ hàng trống</Text>
<Text style={styles.emptySubtext}>
Hãy thêm sản phẩm vào giỏ hàng đ mua sắm
</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Giỏ hàng</Text>
<TouchableOpacity onPress={handleClearCart} style={styles.clearButton}>
<Ionicons name="trash-outline" size={22} color="#ff4444" />
<Text style={styles.clearText}>Xóa tất cả</Text>
</TouchableOpacity>
</View>
<FlatList
data={cart.items}
keyExtractor={(item) => item.cartItemId.toString()}
renderItem={({ item }) => (
<CartItemCard
item={item}
onUpdateQuantity={updateQuantity}
onRemove={removeItem}
/>
)}
contentContainerStyle={styles.listContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={refreshCart}
colors={['#ff6b6b']}
/>
}
/>
<View style={styles.footer}>
<View style={styles.summaryRow}>
<Text style={styles.summaryLabel}>Tổng số lượng:</Text>
<Text style={styles.summaryValue}>{cart.totalItems} sản phẩm</Text>
</View>
<View style={styles.summaryRow}>
<Text style={styles.totalLabel}>Tổng tiền:</Text>
<Text style={styles.totalValue}>
{formatPrice(cart.totalAmount)}
</Text>
</View>
<TouchableOpacity
style={styles.checkoutButton}
onPress={handleCheckout}
>
<Text style={styles.checkoutText}>Thanh toán</Text>
<Ionicons name="arrow-forward" size={20} color="#fff" />
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
headerTitle: {
fontSize: 24,
fontWeight: '700',
color: '#333',
},
clearButton: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
},
clearText: {
fontSize: 14,
color: '#ff4444',
marginLeft: 4,
fontWeight: '500',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: '#666',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 32,
},
emptyText: {
fontSize: 20,
fontWeight: '600',
color: '#666',
marginTop: 16,
},
emptySubtext: {
fontSize: 14,
color: '#999',
marginTop: 8,
textAlign: 'center',
},
listContent: {
padding: 16,
},
footer: {
backgroundColor: '#fff',
padding: 16,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
},
summaryRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
summaryLabel: {
fontSize: 15,
color: '#666',
},
summaryValue: {
fontSize: 15,
color: '#333',
fontWeight: '500',
},
totalLabel: {
fontSize: 18,
fontWeight: '600',
color: '#333',
},
totalValue: {
fontSize: 20,
fontWeight: '700',
color: '#ff6b6b',
},
checkoutButton: {
backgroundColor: '#ff6b6b',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
padding: 16,
borderRadius: 12,
marginTop: 16,
},
checkoutText: {
fontSize: 17,
fontWeight: '700',
color: '#fff',
marginRight: 8,
},
});

View File

@@ -1,18 +1,19 @@
import React from 'react';
import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Image,
ActivityIndicator,
TouchableOpacity,
Dimensions,
Image,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useCart } from '../../../hooks/useCart';
import { useProduct } from '../../../hooks/useProducts';
import { Ionicons } from '@expo/vector-icons';
const { width } = Dimensions.get('window');
@@ -22,6 +23,8 @@ export default function ProductDetailScreen() {
const productId = typeof id === 'string' ? parseInt(id) : null;
const { product, loading, error } = useProduct(productId);
const { addToCart } = useCart();
const [isAddingToCart, setIsAddingToCart] = useState(false);
const formatPrice = (price: number) => {
return new Intl.NumberFormat('vi-VN', {
@@ -38,6 +41,19 @@ export default function ProductDetailScreen() {
});
};
const handleAddToCart = async () => {
if (!product || product.stockQuantity <= 0) {
return;
}
setIsAddingToCart(true);
try {
await addToCart(product.productId, 1);
} finally {
setIsAddingToCart(false);
}
};
if (loading) {
return (
<SafeAreaView style={styles.container}>
@@ -167,16 +183,36 @@ export default function ProductDetailScreen() {
{/* Bottom Action Buttons */}
<View style={styles.bottomActions}>
<TouchableOpacity
style={[styles.actionButton, styles.addToCartButton]}
disabled={product.stockQuantity === 0}
style={[
styles.actionButton,
styles.addToCartButton,
(product.stockQuantity === 0 || isAddingToCart) && styles.actionButtonDisabled
]}
onPress={handleAddToCart}
disabled={product.stockQuantity === 0 || isAddingToCart}
>
<Ionicons name="cart-outline" size={24} color="#fff" />
{isAddingToCart ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Ionicons name="cart-outline" size={24} color="#fff" />
)}
<Text style={styles.actionButtonText}>
{product.stockQuantity > 0 ? 'Thêm vào giỏ' : 'Hết hàng'}
{isAddingToCart
? 'Đang thêm...'
: product.stockQuantity > 0
? 'Thêm vào giỏ'
: 'Hết hàng'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.actionButton, styles.buyNowButton]}>
<TouchableOpacity
style={[
styles.actionButton,
styles.buyNowButton,
product.stockQuantity === 0 && styles.actionButtonDisabled
]}
disabled={product.stockQuantity === 0}
>
<Ionicons name="flash-outline" size={24} color="#fff" />
<Text style={styles.actionButtonText}>Mua ngay</Text>
</TouchableOpacity>
@@ -344,6 +380,9 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: '#fff',
},
actionButtonDisabled: {
opacity: 0.5,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',