579 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
View,
Text,
ScrollView,
TouchableOpacity,
Image,
TextInput,
LayoutAnimation,
Platform,
UIManager,
Animated,
StatusBar,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './cartScreen.styles';
import { Header, QuantitySelector, PrimaryButton } from '@components';
import { useAppTheme } from '@theme';
import {
selectCartTotal,
getCartThunk,
updateCartItemThunk,
} from '../../../store/commonreducers/cart';
import { AppStackParamList } from '../../../navigation/appStack';
import { useAppDispatch, useAppSelector } from '@store';
import { getFullUrl } from '@utils';
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
const TIP_OPTIONS = [20, 30, 50, 100];
// ─── Skeleton shimmer ──────────────────────────────────────────────────────────
const SkeletonBlock: React.FC<{ width?: number | string; height?: number; borderRadius?: number; colors: any }> = ({
width = '100%',
height = 16,
borderRadius = 8,
colors,
}) => {
const shimmer = useRef(new Animated.Value(0)).current;
useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(shimmer, { toValue: 1, duration: 900, useNativeDriver: true }),
Animated.timing(shimmer, { toValue: 0, duration: 900, useNativeDriver: true }),
]),
);
loop.start();
return () => loop.stop();
}, [shimmer]);
const opacity = shimmer.interpolate({ inputRange: [0, 1], outputRange: [0.3, 0.7] });
return (
<Animated.View
style={{
width: width as any,
height,
borderRadius,
backgroundColor: colors.border,
opacity,
marginBottom: 8,
}}
/>
);
};
// ─── Savings pill ──────────────────────────────────────────────────────────────
const SavingsPill: React.FC<{ label: string; styles: any }> = ({ label, styles }) => (
<View style={styles.savingsPill}>
<Text style={styles.savingsPillEmoji}>🎉</Text>
<Text style={styles.savingsPillText}>{label}</Text>
</View>
);
// ─── Section Header ────────────────────────────────────────────────────────────
const SectionTitle: React.FC<{ label: string; styles: any }> = ({ label, styles }) => (
<Text style={styles.sectionTitle}>{label}</Text>
);
// ─── Main Component ────────────────────────────────────────────────────────────
export const CartScreen: React.FC = () => {
const { colors, isDarkMode } = useAppTheme();
const styles = getStyles(colors);
const dispatch = useAppDispatch();
const navigation = useNavigation<CartScreenNavProp>();
const { items, isLoading } = useAppSelector(state => state.cart);
const totals = useAppSelector(selectCartTotal);
const [instructions, setInstructions] = useState('');
const [showInstructionsInput, setShowInstructionsInput] = useState(false);
const [selectedTip, setSelectedTip] = useState<number | null>(null);
const [couponInput, setCouponInput] = useState('');
const [billExpanded, setBillExpanded] = useState(false);
// Animated values
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(30)).current;
const bottomPanelAnim = useRef(new Animated.Value(100)).current;
useEffect(() => {
dispatch(getCartThunk());
}, [dispatch]);
useEffect(() => {
if (!isLoading) {
Animated.parallel([
Animated.timing(fadeAnim, { toValue: 1, duration: 400, useNativeDriver: true }),
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, tension: 80, friction: 10 }),
Animated.spring(bottomPanelAnim, { toValue: 0, useNativeDriver: true, tension: 80, friction: 12 }),
]).start();
}
}, [isLoading, fadeAnim, slideAnim, bottomPanelAnim]);
const itemCount = useMemo(
() => items.reduce((sum, item) => sum + item.quantity, 0),
[items],
);
const tipAmount = selectedTip ?? 0;
const grandTotal = (totals.total ?? 0) + tipAmount;
const toggleBillExpanded = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setBillExpanded(prev => !prev);
};
// ─── Empty state ──────────────────────────────────────────────────────────────
if (!isLoading && items.length === 0) {
return (
<View style={styles.container}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<Header title="My Cart" onBack={() => navigation.goBack()} />
<View style={styles.emptyWrap}>
<View style={styles.emptyIllustration}>
<Text style={styles.emptyEmoji}>🛒</Text>
</View>
<Text style={styles.emptyTitle}>Your cart is empty</Text>
<Text style={styles.emptySubtitle}>
Looks like you haven't added anything yet.{'\n'}Browse and find something you'll love!
</Text>
<TouchableOpacity
style={styles.emptyButton}
activeOpacity={0.85}
onPress={() => navigation.goBack()}
>
<Text style={styles.emptyButtonText}>Browse Menu</Text>
</TouchableOpacity>
</View>
</View>
);
}
// ─── Loading skeleton ─────────────────────────────────────────────────────────
if (isLoading) {
return (
<View style={styles.container}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<Header title="My Cart" onBack={() => navigation.goBack()} />
<ScrollView contentContainerStyle={styles.list} showsVerticalScrollIndicator={false}>
<View style={[styles.skeletonCard, { marginTop: 12 }]}>
{[1, 2, 3].map(i => (
<View key={i} style={styles.skeletonItemRow}>
<SkeletonBlock width={56} height={56} borderRadius={12} colors={colors} />
<View style={{ flex: 1, marginLeft: 12 }}>
<SkeletonBlock width="80%" height={14} colors={colors} />
<SkeletonBlock width="40%" height={12} colors={colors} />
</View>
<SkeletonBlock width={80} height={32} borderRadius={8} colors={colors} />
</View>
))}
</View>
<View style={styles.skeletonCard}>
<SkeletonBlock height={60} colors={colors} />
</View>
<View style={styles.skeletonCard}>
<SkeletonBlock height={120} colors={colors} />
</View>
</ScrollView>
</View>
);
}
// ─── Main Cart View ───────────────────────────────────────────────────────────
return (
<View style={styles.container}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<Header title="My Cart" onBack={() => navigation.goBack()} />
<Animated.View
style={[{ flex: 1 }, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}
>
<ScrollView
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
>
{/* ── ETA Banner ─────────────────────────────────────────── */}
<View style={styles.etaBanner}>
<View style={styles.etaLeft}>
<View style={styles.etaIconWrap}>
<Text style={styles.etaIcon}>🛵</Text>
</View>
<View>
<Text style={styles.etaText}>Delivery in 2025 mins</Text>
<Text style={styles.etaSubtext}>Order arrives fresh & hot 🔥</Text>
</View>
</View>
<View style={styles.etaBadge}>
<Text style={styles.etaBadgeText}>FASTEST</Text>
</View>
</View>
{/* ── Items Card ─────────────────────────────────────────── */}
<View style={styles.itemsCard}>
<View style={styles.itemsCardHeader}>
<Text style={styles.itemsCardHeaderText}>
{itemCount} {itemCount === 1 ? 'item' : 'items'} in your cart
</Text>
</View>
{items.map((item, index) => (
<View
key={item.id}
style={[
styles.cartItem,
index < items.length - 1 && styles.cartItemDivider,
]}
>
{/* Veg / Non-veg indicator */}
{/* {typeof item.product === 'boolean' && (
<View
style={[
styles.vegIndicator,
{ borderColor: item.product ? '#2E7D32' : '#C62828' },
]}
>
<View
style={[
styles.vegDot,
{ backgroundColor: item.product ? '#2E7D32' : '#C62828' },
]}
/>
</View>
)} */}
{/* Thumbnail */}
<View style={styles.itemThumbWrap}>
<Image
style={styles.itemThumb}
source={{ uri: getFullUrl(item.product.imageUrl) }}
resizeMode="cover"
/>
</View>
{/* Info */}
<View style={styles.itemInfo}>
<Text style={styles.itemTitle} numberOfLines={2}>
{item.product.name}
</Text>
<View style={styles.itemPriceRow}>
<Text style={styles.itemPrice}>{item.product.price}</Text>
{/* {!!item.product.mrp && item.product.mrp > item.product.price && (
<>
<Text style={styles.itemMrp}>₹{item.product.mrp}</Text>
<View style={styles.discountBadge}>
<Text style={styles.discountBadgeText}>
{Math.round(((item.product.mrp - item.product.price) / item.product.mrp) * 100)}% OFF
</Text>
</View>
</>
)} */}
</View>
</View>
{/* Quantity */}
<QuantitySelector
value={item.quantity}
onIncrement={() =>
dispatch(
updateCartItemThunk({
productId: item.productId,
quantity: item.quantity + 1,
}),
)
}
onDecrement={() => {
if (item.quantity >= 1) {
dispatch(
updateCartItemThunk({
productId: item.productId,
quantity: item.quantity - 1,
}),
);
}
}}
/>
</View>
))}
{/* Add more items */}
<TouchableOpacity
style={styles.addMoreRow}
activeOpacity={0.7}
onPress={() => navigation.goBack()}
>
<Text style={styles.addMoreIcon}></Text>
<Text style={styles.addMoreText}>Add more items</Text>
</TouchableOpacity>
</View>
{/* ── Savings if any ─────────────────────────────────────── */}
{totals.discount > 0 && (
<SavingsPill
label={`You're saving ₹${totals.discount} on this order 🎉`}
styles={styles}
/>
)}
{/* ── Coupons & Offers ───────────────────────────────────── */}
<View style={styles.section}>
<SectionTitle label="Coupons & Offers" styles={styles} />
{totals.discount > 0 ? (
<View style={styles.couponCard}>
<View style={styles.couponAppliedRow}>
<View style={styles.couponAppliedLeft}>
<View style={styles.couponCheckBadge}>
<Text style={styles.couponCheckIcon}>🏷</Text>
</View>
<View>
<Text style={styles.couponAppliedCode}>Coupon Applied!</Text>
<Text style={styles.couponAppliedSub}>
You saved {totals.discount} on this order
</Text>
</View>
</View>
<TouchableOpacity style={styles.couponRemoveBtn}>
<Text style={styles.couponRemoveText}>Remove</Text>
</TouchableOpacity>
</View>
</View>
) : (
<View style={styles.couponCard}>
<View style={styles.couponInputRow}>
<Text style={styles.couponIcon}>🎁</Text>
<TextInput
style={styles.couponInput}
placeholder="Enter coupon code"
placeholderTextColor={colors.placeholder}
value={couponInput}
onChangeText={setCouponInput}
autoCapitalize="characters"
/>
<TouchableOpacity
style={[
styles.applyButton,
!couponInput && styles.applyButtonDisabled,
]}
disabled={!couponInput}
activeOpacity={0.85}
>
<Text style={styles.applyButtonText}>Apply</Text>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.viewCouponsRow} activeOpacity={0.7} onPress={() => navigation.navigate('MainTabs', { screen: 'OffersScreen' })}>
<View style={styles.viewCouponsLeft}>
<Text style={styles.viewCouponsTag}>🔖</Text>
<Text style={styles.viewCouponsText}>View all available offers</Text>
</View>
<Text style={styles.viewCouponsChevron}></Text>
</TouchableOpacity>
</View>
)}
</View>
{/* ── Order Instructions ─────────────────────────────────── */}
{/* <View style={styles.section}>
<SectionTitle label="Order Instructions" styles={styles} />
<View style={styles.instructionsCard}>
{showInstructionsInput ? (
<TextInput
style={styles.instructionsInput}
placeholder="e.g. No onions, leave at door, don't ring bell…"
placeholderTextColor={colors.placeholder}
value={instructions}
onChangeText={setInstructions}
multiline
autoFocus
onBlur={() => setShowInstructionsInput(false)}
/>
) : (
<TouchableOpacity
style={styles.instructionsRow}
activeOpacity={0.7}
onPress={() => setShowInstructionsInput(true)}
>
<Text style={styles.instructionsIcon}>📝</Text>
<Text
style={
instructions
? styles.instructionsFilledText
: styles.instructionsPlaceholder
}
numberOfLines={1}
>
{instructions || 'Add a note for the rider or restaurant'}
</Text>
<Text style={styles.instructionsChevron}></Text>
</TouchableOpacity>
)}
</View>
</View> */}
{/* ── Tip Your Delivery Partner ──────────────────────────── */}
{/* <View style={styles.section}>
<SectionTitle label="Tip Your Delivery Partner 💛" styles={styles} />
<View style={styles.tipCard}>
<Text style={styles.tipSubtitle}>
100% of the tip goes directly to your delivery partner. They
work hard so your food arrives hot!
</Text>
<View style={styles.tipOptionsRow}>
{TIP_OPTIONS.map(amount => (
<TouchableOpacity
key={amount}
style={[
styles.tipOption,
selectedTip === amount && styles.tipOptionSelected,
]}
activeOpacity={0.8}
onPress={() =>
setSelectedTip(prev => (prev === amount ? null : amount))
}
>
{selectedTip === amount && (
<Text style={styles.tipSelectedCheck}>✓ </Text>
)}
<Text
style={[
styles.tipOptionText,
selectedTip === amount && styles.tipOptionTextSelected,
]}
>
₹{amount}
</Text>
</TouchableOpacity>
))}
</View>
{selectedTip !== null && (
<TouchableOpacity
style={styles.tipRemove}
onPress={() => setSelectedTip(null)}
>
<Text style={styles.tipRemoveText}>✕ Remove tip</Text>
</TouchableOpacity>
)}
</View>
</View> */}
{/* ── Bill Details ───────────────────────────────────────── */}
<View style={styles.section}>
<SectionTitle label="Bill Details" styles={styles} />
<View style={styles.billCard}>
<View style={styles.feeRow}>
<Text style={styles.feeLabel}>Item Total</Text>
<Text style={styles.feeValue}>{totals.subtotal}</Text>
</View>
<View style={styles.feeRow}>
<TouchableOpacity
style={styles.feeLabelRow}
onPress={toggleBillExpanded}
activeOpacity={0.7}
>
<Text style={styles.feeLabel}>Delivery Fee</Text>
<Text style={styles.infoIcon}> </Text>
</TouchableOpacity>
<Text style={styles.feeValue}>{totals.deliveryFee}</Text>
</View>
{billExpanded && (
<View style={styles.gstNote}>
<Text style={styles.gstNoteText}>
Delivery fee helps cover your delivery partner's costs. GST as
applicable is included in the item total.
</Text>
</View>
)}
<View style={styles.feeRow}>
<Text style={styles.feeLabel}>Platform Fee</Text>
<Text style={styles.feeValue}>₹{totals.platformFee}</Text>
</View>
{tipAmount > 0 && (
<View style={styles.feeRow}>
<Text style={styles.feeLabel}>Delivery Tip 💛</Text>
<Text style={styles.feeValue}>₹{tipAmount}</Text>
</View>
)}
{totals.discount > 0 && (
<View style={styles.feeRow}>
<Text style={[styles.feeLabel, styles.discountLabel]}>
Coupon Discount
</Text>
<Text style={[styles.feeValue, styles.discountValue]}>
₹{totals.discount}
</Text>
</View>
)}
<View style={styles.billDivider} />
<View style={styles.totalRow}>
<View>
<Text style={styles.totalLabel}>To Pay</Text>
{totals.discount > 0 && (
<Text style={styles.totalSaved}>
Saved ₹{totals.discount}
</Text>
)}
</View>
<Text style={styles.totalValue}>₹{grandTotal}</Text>
</View>
</View>
</View>
{/* ── Cancellation Policy ────────────────────────────────── */}
<View style={styles.policyCard}>
<Text style={styles.policyIcon}>🛈</Text>
<Text style={styles.policyText}>
Orders cannot be cancelled once packed for delivery. In case of
unexpected delays, a refund will be provided, if applicable.
</Text>
</View>
</ScrollView>
</Animated.View>
{/* ── Bottom Panel ───────────────────────────────────────────── */}
<Animated.View
style={[styles.bottomPanel, { transform: [{ translateY: bottomPanelAnim }] }]}
>
<View style={styles.bottomTotalWrap}>
<Text style={styles.bottomTotalLabel}>
{itemCount} {itemCount === 1 ? 'item' : 'items'}
</Text>
<View style={styles.bottomTotalRow}>
<Text style={styles.bottomTotal}>₹{grandTotal}</Text>
{totals.discount > 0 && (
<View style={styles.bottomSavedBadge}>
<Text style={styles.bottomSavedText}>saved ₹{totals.discount}</Text>
</View>
)}
</View>
</View>
<PrimaryButton
title="Proceed to Checkout →"
onPress={() => navigation.navigate('CheckoutAddressScreen')}
style={styles.checkoutButton}
/>
</Animated.View>
</View>
);
};