From 1aed769b620b1897536e575b2411d5a56f592398 Mon Sep 17 00:00:00 2001 From: Atanu Das Date: Fri, 31 Jul 2026 18:29:12 +0530 Subject: [PATCH] feat: implement offers screen with redux state management and interfaces --- .../screens/cartScreen/cartScreen.styles.ts | 626 +++++++++++++----- .../screens/cartScreen/cartScreen.tsx | 614 ++++++++++++++--- .../offersScreen/offersScreen.styles.ts | 223 +++++-- .../screens/offersScreen/offersScreen.tsx | 259 ++++++-- app/interfaces/offers.ts | 4 +- app/store/commonreducers/offer/reducer.ts | 8 +- 6 files changed, 1373 insertions(+), 361 deletions(-) diff --git a/app/features/screens/cartScreen/cartScreen.styles.ts b/app/features/screens/cartScreen/cartScreen.styles.ts index 213e0e0..0cb2a0c 100644 --- a/app/features/screens/cartScreen/cartScreen.styles.ts +++ b/app/features/screens/cartScreen/cartScreen.styles.ts @@ -1,110 +1,224 @@ import { StyleSheet, Platform } from 'react-native'; import { typography } from '@theme'; +// ─── Shared shadows ──────────────────────────────────────────────────────────── +const cardShadow = Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.07, + shadowRadius: 12, + }, + android: { elevation: 3 }, +}); + +const heavyShadow = Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: -6 }, + shadowOpacity: 0.12, + shadowRadius: 16, + }, + android: { elevation: 16 }, +}); + export const getStyles = (colors: any) => StyleSheet.create({ + // ── Layout ────────────────────────────────────────────────────────────────── container: { flex: 1, backgroundColor: colors.background, }, list: { paddingBottom: 140, + paddingTop: 4, }, - // ---------- ETA banner ---------- + // ── Section wrapper ───────────────────────────────────────────────────────── + section: { + paddingHorizontal: 16, + marginTop: 22, + }, + sectionTitle: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 10, + letterSpacing: 0.1, + }, + + // ── ETA Banner ────────────────────────────────────────────────────────────── etaBanner: { flexDirection: 'row', alignItems: 'center', - backgroundColor: colors.primaryMuted ?? '#E9F7EF', - marginHorizontal: 16, - marginTop: 12, - marginBottom: 4, - borderRadius: 12, - paddingVertical: 10, - paddingHorizontal: 14, - }, - etaIcon: { - fontSize: 16, - marginRight: 8, - }, - etaText: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.semibold, - color: colors.primary, - }, - - // ---------- Item cards ---------- - itemsCard: { - backgroundColor: colors.cardBg, + justifyContent: 'space-between', + backgroundColor: colors.primaryMuted ?? '#E8F5EE', marginHorizontal: 16, marginTop: 14, borderRadius: 16, + paddingVertical: 14, + paddingHorizontal: 14, + borderWidth: 1, + borderColor: colors.primary + '22', + }, + etaLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + etaIconWrap: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: colors.primary + '18', + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + etaIcon: { + fontSize: 20, + }, + etaText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.bold, + color: colors.primary, + }, + etaSubtext: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + marginTop: 2, + }, + etaBadge: { + backgroundColor: colors.primary, + borderRadius: 8, + paddingHorizontal: 10, + paddingVertical: 5, + }, + etaBadgeText: { + color: '#FFFFFF', + fontSize: 10, + fontWeight: typography.fontWeight.bold, + letterSpacing: 1, + }, + + // ── Items Card ────────────────────────────────────────────────────────────── + itemsCard: { + backgroundColor: colors.cardBg, + marginHorizontal: 16, + marginTop: 16, + borderRadius: 20, overflow: 'hidden', - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, - }, - android: { elevation: 1 }, - }), + ...cardShadow, + }, + itemsCardHeader: { + paddingHorizontal: 16, + paddingTop: 14, + paddingBottom: 6, + borderBottomWidth: 1, + borderBottomColor: colors.border, + }, + itemsCardHeaderText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + color: colors.textSecondary, + textTransform: 'uppercase', + letterSpacing: 0.8, }, cartItem: { flexDirection: 'row', alignItems: 'center', paddingVertical: 14, - paddingHorizontal: 14, + paddingHorizontal: 16, }, cartItemDivider: { borderBottomWidth: 1, borderBottomColor: colors.border, }, - itemThumb: { - width: 52, - height: 52, - borderRadius: 10, - backgroundColor: colors.inputBg, + + // Veg indicator (left border dot) + vegIndicator: { + width: 14, + height: 14, + borderRadius: 3, + borderWidth: 1.5, alignItems: 'center', justifyContent: 'center', + marginRight: 10, + flexShrink: 0, + }, + vegDot: { + width: 6, + height: 6, + borderRadius: 3, + }, + + itemThumbWrap: { marginRight: 12, }, - itemThumbEmoji: { - fontSize: 22, + itemThumb: { + width: 60, + height: 60, + borderRadius: 12, + backgroundColor: colors.inputBg, }, itemInfo: { flex: 1, - marginRight: 10, + marginRight: 8, }, itemTitle: { fontSize: typography.fontSize.md, fontWeight: typography.fontWeight.semibold, color: colors.text, - marginBottom: 4, + marginBottom: 6, + lineHeight: 20, + }, + itemPriceRow: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + gap: 6, }, itemPrice: { fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.bold, + color: colors.text, + }, + itemMrp: { + fontSize: typography.fontSize.xs, color: colors.textSecondary, + textDecorationLine: 'line-through', + }, + discountBadge: { + backgroundColor: colors.primary + '18', + borderRadius: 4, + paddingHorizontal: 5, + paddingVertical: 2, + }, + discountBadgeText: { + fontSize: 10, + fontWeight: typography.fontWeight.bold, + color: colors.primary, }, - // ---------- Add more items ---------- + // ── Add more items row ───────────────────────────────────────────────────── addMoreRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - paddingVertical: 12, - marginHorizontal: 16, + paddingVertical: 14, + marginHorizontal: 14, + marginBottom: 14, marginTop: 4, borderWidth: 1.5, borderStyle: 'dashed', - borderColor: colors.border, + borderColor: colors.primary + '55', borderRadius: 12, }, addMoreIcon: { - fontSize: 14, + fontSize: 16, marginRight: 6, color: colors.primary, + fontWeight: typography.fontWeight.bold, }, addMoreText: { fontSize: typography.fontSize.sm, @@ -112,68 +226,100 @@ export const getStyles = (colors: any) => color: colors.primary, }, - // ---------- Footer ---------- - footer: { - paddingHorizontal: 16, - paddingTop: 20, - }, - - // Coupon - sectionTitle: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.bold, - color: colors.text, - marginBottom: 10, - }, - couponCard: { - backgroundColor: colors.cardBg, - borderRadius: 14, - padding: 6, - marginBottom: 20, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, - }, - android: { elevation: 1 }, - }), - }, - couponRow: { + // ── Savings Pill ─────────────────────────────────────────────────────────── + savingsPill: { flexDirection: 'row', alignItems: 'center', + backgroundColor: '#FFF3E0', + marginHorizontal: 16, + marginTop: 16, + borderRadius: 12, + paddingVertical: 10, + paddingHorizontal: 14, + }, + savingsPillEmoji: { + fontSize: 16, + marginRight: 8, + }, + savingsPillText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: '#E65100', + }, + + // ── Coupon Card ──────────────────────────────────────────────────────────── + couponCard: { + backgroundColor: colors.cardBg, + borderRadius: 16, + overflow: 'hidden', + ...cardShadow, + }, + couponInputRow: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 14, + paddingVertical: 4, + borderBottomWidth: 1, + borderBottomColor: colors.border, }, couponIcon: { fontSize: 18, - marginLeft: 10, - marginRight: 4, + marginRight: 8, }, couponInput: { flex: 1, - height: 44, - paddingHorizontal: 8, - fontSize: typography.fontSize.md, + height: 48, + fontSize: typography.fontSize.sm, color: colors.text, + fontWeight: typography.fontWeight.medium, }, applyButton: { paddingHorizontal: 18, - paddingVertical: 11, + paddingVertical: 9, borderRadius: 10, backgroundColor: colors.primary, - marginRight: 4, + }, + applyButtonDisabled: { + opacity: 0.35, }, applyButtonText: { color: '#FFFFFF', - fontWeight: typography.fontWeight.semibold, + fontWeight: typography.fontWeight.bold, fontSize: typography.fontSize.sm, }, + viewCouponsRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + paddingHorizontal: 14, + }, + viewCouponsLeft: { + flexDirection: 'row', + alignItems: 'center', + }, + viewCouponsTag: { + fontSize: 15, + marginRight: 8, + }, + viewCouponsText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + }, + viewCouponsChevron: { + fontSize: 20, + color: colors.textSecondary, + lineHeight: 22, + }, + + // Applied coupon couponAppliedRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingVertical: 10, - paddingHorizontal: 12, + paddingVertical: 14, + paddingHorizontal: 14, }, couponAppliedLeft: { flexDirection: 'row', @@ -181,16 +327,16 @@ export const getStyles = (colors: any) => flex: 1, }, couponCheckBadge: { - width: 28, - height: 28, - borderRadius: 14, - backgroundColor: colors.primaryMuted ?? '#E9F7EF', + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: colors.primary + '18', alignItems: 'center', justifyContent: 'center', - marginRight: 10, + marginRight: 12, }, couponCheckIcon: { - fontSize: 13, + fontSize: 16, }, couponAppliedCode: { fontSize: typography.fontSize.sm, @@ -200,80 +346,225 @@ export const getStyles = (colors: any) => couponAppliedSub: { fontSize: typography.fontSize.xs, color: colors.primary, - marginTop: 1, + marginTop: 2, + fontWeight: typography.fontWeight.medium, + }, + couponRemoveBtn: { + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 8, + borderWidth: 1, + borderColor: '#D32F2F' + '44', }, couponRemoveText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + color: '#D32F2F', + }, + + // ── Instructions Card ────────────────────────────────────────────────────── + instructionsCard: { + backgroundColor: colors.cardBg, + borderRadius: 16, + overflow: 'hidden', + ...cardShadow, + }, + instructionsRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 16, + paddingHorizontal: 14, + }, + instructionsIcon: { + fontSize: 18, + marginRight: 10, + }, + instructionsPlaceholder: { + flex: 1, fontSize: typography.fontSize.sm, + color: colors.placeholder, + }, + instructionsFilledText: { + flex: 1, + fontSize: typography.fontSize.sm, + color: colors.text, + fontWeight: typography.fontWeight.medium, + }, + instructionsChevron: { + fontSize: 22, + color: colors.textSecondary, + lineHeight: 24, + }, + instructionsInput: { + minHeight: 80, + padding: 14, + fontSize: typography.fontSize.sm, + color: colors.text, + textAlignVertical: 'top', + lineHeight: 20, + }, + + // ── Tip Card ─────────────────────────────────────────────────────────────── + tipCard: { + backgroundColor: colors.cardBg, + borderRadius: 16, + padding: 14, + ...cardShadow, + }, + tipSubtitle: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + marginBottom: 14, + lineHeight: 18, + }, + tipOptionsRow: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + gap: 10, + }, + tipOption: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1.5, + borderColor: colors.border, + borderRadius: 12, + paddingVertical: 9, + paddingHorizontal: 16, + }, + tipOptionSelected: { + borderColor: colors.primary, + backgroundColor: colors.primary + '14', + }, + tipSelectedCheck: { + fontSize: 11, + color: colors.primary, + fontWeight: typography.fontWeight.bold, + }, + tipOptionText: { + fontSize: typography.fontSize.sm, + fontWeight: typography.fontWeight.semibold, + color: colors.text, + }, + tipOptionTextSelected: { + color: colors.primary, + }, + tipRemove: { + marginTop: 12, + alignSelf: 'flex-start', + paddingVertical: 4, + }, + tipRemoveText: { + fontSize: typography.fontSize.xs, fontWeight: typography.fontWeight.semibold, color: '#D32F2F', }, - // Bill details + // ── Bill Card ────────────────────────────────────────────────────────────── billCard: { backgroundColor: colors.cardBg, - borderRadius: 14, + borderRadius: 16, padding: 16, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, - }, - android: { elevation: 1 }, - }), + ...cardShadow, }, feeRow: { flexDirection: 'row', justifyContent: 'space-between', - marginBottom: 10, + alignItems: 'center', + marginBottom: 12, + }, + feeLabelRow: { + flexDirection: 'row', + alignItems: 'center', }, feeLabel: { fontSize: typography.fontSize.sm, color: colors.textSecondary, }, + infoIcon: { + fontSize: 12, + color: colors.primary, + fontWeight: typography.fontWeight.bold, + }, feeValue: { fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.medium, color: colors.text, }, + discountLabel: { + color: colors.primary, + fontWeight: typography.fontWeight.semibold, + }, + discountValue: { + color: colors.primary, + fontWeight: typography.fontWeight.bold, + }, + gstNote: { + backgroundColor: colors.inputBg, + borderRadius: 10, + padding: 10, + marginBottom: 12, + marginTop: -4, + }, + gstNoteText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + lineHeight: 17, + }, + billDivider: { + height: 1, + backgroundColor: colors.border, + marginVertical: 8, + }, totalRow: { - borderTopWidth: 1, - borderTopColor: colors.border, - paddingTop: 12, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', marginTop: 4, - marginBottom: 0, }, totalLabel: { fontSize: typography.fontSize.md, fontWeight: typography.fontWeight.bold, color: colors.text, }, + totalSaved: { + fontSize: typography.fontSize.xs, + color: colors.primary, + marginTop: 2, + fontWeight: typography.fontWeight.semibold, + }, totalValue: { - fontSize: typography.fontSize.md, + fontSize: 20, fontWeight: typography.fontWeight.bold, color: colors.text, }, - savingsBanner: { + + // ── Policy Card ──────────────────────────────────────────────────────────── + policyCard: { flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.primaryMuted ?? '#E9F7EF', - borderRadius: 10, - paddingVertical: 8, - paddingHorizontal: 12, - marginTop: 14, + alignItems: 'flex-start', + marginHorizontal: 16, + marginTop: 20, + marginBottom: 8, + backgroundColor: colors.inputBg, + borderRadius: 12, + paddingVertical: 12, + paddingHorizontal: 14, }, - savingsIcon: { + policyIcon: { fontSize: 14, - marginRight: 6, + color: colors.textSecondary, + marginRight: 10, + marginTop: 1, }, - savingsText: { + policyText: { + flex: 1, fontSize: typography.fontSize.xs, - fontWeight: typography.fontWeight.semibold, - color: colors.primary, + color: colors.textSecondary, + lineHeight: 18, }, - // ---------- Bottom panel ---------- + // ── Bottom Panel ─────────────────────────────────────────────────────────── bottomPanel: { position: 'absolute', left: 0, @@ -283,68 +574,107 @@ export const getStyles = (colors: any) => alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: Platform.OS === 'ios' ? 28 : 16, + paddingTop: 14, + paddingBottom: Platform.OS === 'ios' ? 30 : 16, backgroundColor: colors.cardBg, - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: -4 }, - shadowOpacity: 0.08, - shadowRadius: 12, - }, - android: { elevation: 10 }, - }), + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + ...heavyShadow, + }, + bottomTotalWrap: { + flex: 1, }, - bottomTotalWrap: {}, bottomTotalLabel: { fontSize: typography.fontSize.xs, color: colors.textSecondary, + letterSpacing: 0.3, + marginBottom: 2, + }, + bottomTotalRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, }, bottomTotal: { fontSize: typography.fontSize.lg, fontWeight: typography.fontWeight.bold, color: colors.text, }, + bottomSavedBadge: { + backgroundColor: colors.primary + '18', + borderRadius: 6, + paddingHorizontal: 7, + paddingVertical: 3, + }, + bottomSavedText: { + fontSize: 10, + fontWeight: typography.fontWeight.bold, + color: colors.primary, + }, checkoutButton: { flex: 1, - marginLeft: 16, + marginLeft: 14, }, - // ---------- Empty state ---------- + // ── Empty State ──────────────────────────────────────────────────────────── emptyWrap: { flex: 1, alignItems: 'center', justifyContent: 'center', - paddingHorizontal: 32, + paddingHorizontal: 36, + }, + emptyIllustration: { + width: 110, + height: 110, + borderRadius: 55, + backgroundColor: colors.inputBg, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 24, + ...cardShadow, }, emptyEmoji: { - fontSize: 56, - marginBottom: 16, + fontSize: 52, }, emptyTitle: { - fontSize: typography.fontSize.lg, + fontSize: typography.fontSize.xl, fontWeight: typography.fontWeight.bold, color: colors.text, - marginBottom: 6, + marginBottom: 10, }, emptySubtitle: { fontSize: typography.fontSize.sm, color: colors.textSecondary, textAlign: 'center', - marginBottom: 24, + lineHeight: 22, + marginBottom: 30, }, emptyButton: { backgroundColor: colors.primary, - paddingHorizontal: 28, - paddingVertical: 14, - borderRadius: 12, + paddingHorizontal: 36, + paddingVertical: 15, + borderRadius: 14, + ...cardShadow, }, emptyButtonText: { color: '#FFFFFF', fontWeight: typography.fontWeight.bold, fontSize: typography.fontSize.md, + letterSpacing: 0.3, }, - }); + + // ── Skeleton ─────────────────────────────────────────────────────────────── + skeletonCard: { + backgroundColor: colors.cardBg, + marginHorizontal: 16, + marginTop: 14, + borderRadius: 20, + padding: 16, + ...cardShadow, + }, + skeletonItemRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + }); \ No newline at end of file diff --git a/app/features/screens/cartScreen/cartScreen.tsx b/app/features/screens/cartScreen/cartScreen.tsx index 3d441b2..f316108 100644 --- a/app/features/screens/cartScreen/cartScreen.tsx +++ b/app/features/screens/cartScreen/cartScreen.tsx @@ -1,5 +1,17 @@ -import React, { useEffect } from 'react'; -import { View, Text, ScrollView, TouchableOpacity, Image } from 'react-native'; +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'; @@ -13,171 +25,555 @@ import { import { AppStackParamList } from '../../../navigation/appStack'; import { useAppDispatch, useAppSelector } from '@store'; import { getFullUrl } from '@utils'; -// import { getFullUrl } from '@utils/helperr'; + +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} type CartScreenNavProp = StackNavigationProp; +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 ( + + ); +}; + +// ─── Savings pill ────────────────────────────────────────────────────────────── +const SavingsPill: React.FC<{ label: string; styles: any }> = ({ label, styles }) => ( + + 🎉 + {label} + +); + +// ─── Section Header ──────────────────────────────────────────────────────────── +const SectionTitle: React.FC<{ label: string; styles: any }> = ({ label, styles }) => ( + {label} +); + +// ─── Main Component ──────────────────────────────────────────────────────────── export const CartScreen: React.FC = () => { - const { colors } = useAppTheme(); + const { colors, isDarkMode } = useAppTheme(); const styles = getStyles(colors); const dispatch = useAppDispatch(); const navigation = useNavigation(); const { items, isLoading } = useAppSelector(state => state.cart); const totals = useAppSelector(selectCartTotal); + const [instructions, setInstructions] = useState(''); + const [showInstructionsInput, setShowInstructionsInput] = useState(false); + const [selectedTip, setSelectedTip] = useState(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 ( -
navigation.goBack()} /> + +
navigation.goBack()} /> - 🛒 + + 🛒 + Your cart is empty - Looks like you haven't added anything yet. Browse providers and find - something you'll love. + Looks like you haven't added anything yet.{'\n'}Browse and find something you'll love! navigation.goBack()} > - Start Ordering + Browse Menu ); } + // ─── Loading skeleton ───────────────────────────────────────────────────────── + if (isLoading) { + return ( + + +
navigation.goBack()} /> + + + {[1, 2, 3].map(i => ( + + + + + + + + + ))} + + + + + + + + + + ); + } + + // ─── Main Cart View ─────────────────────────────────────────────────────────── return ( -
navigation.goBack()} /> + +
navigation.goBack()} /> - - - 🛵 - Delivery in 20–25 mins - - - - {items.map((item, index) => ( - - - - - {item.product.name} - - ₹{item.product.price} + + {/* ── ETA Banner ─────────────────────────────────────────── */} + + + + 🛵 - - dispatch( - updateCartItemThunk({ - productId: item.productId, - quantity: item.quantity + 1, - }), - ) - } - onDecrement={() => { - if (item.quantity >= 1) { + + Delivery in 20–25 mins + Order arrives fresh & hot 🔥 + + + + FASTEST + + + + {/* ── Items Card ─────────────────────────────────────────── */} + + + + {itemCount} {itemCount === 1 ? 'item' : 'items'} in your cart + + + + {items.map((item, index) => ( + + {/* Veg / Non-veg indicator */} + {/* {typeof item.product === 'boolean' && ( + + + + )} */} + + {/* Thumbnail */} + + + + + {/* Info */} + + + {item.product.name} + + + ₹{item.product.price} + {/* {!!item.product.mrp && item.product.mrp > item.product.price && ( + <> + ₹{item.product.mrp} + + + {Math.round(((item.product.mrp - item.product.price) / item.product.mrp) * 100)}% OFF + + + + )} */} + + + + {/* Quantity */} + dispatch( updateCartItemThunk({ productId: item.productId, - quantity: item.quantity - 1, + quantity: item.quantity + 1, }), - ); + ) } - }} - /> - - ))} - + onDecrement={() => { + if (item.quantity >= 1) { + dispatch( + updateCartItemThunk({ + productId: item.productId, + quantity: item.quantity - 1, + }), + ); + } + }} + /> + + ))} - navigation.goBack()} - > - + - Add more items - - - - {/* Coupon logic removed/commented out for now as requested - Apply Coupon - - ... + {/* Add more items */} + navigation.goBack()} + > + + Add more items + - */} - Bill Details - - - Subtotal - ₹{totals.subtotal} + {/* ── Savings if any ─────────────────────────────────────── */} + {totals.discount > 0 && ( + + )} + + {/* ── Coupons & Offers ───────────────────────────────────── */} + + + + {totals.discount > 0 ? ( + + + + + 🏷️ + + + Coupon Applied! + + You saved ₹{totals.discount} on this order + + + + + Remove + + + + ) : ( + + + 🎁 + + + Apply + + + navigation.navigate('MainTabs', { screen: 'OffersScreen' })}> + + 🔖 + View all available offers + + + + + )} + + + {/* ── Order Instructions ─────────────────────────────────── */} + {/* + + + {showInstructionsInput ? ( + setShowInstructionsInput(false)} + /> + ) : ( + setShowInstructionsInput(true)} + > + 📝 + + {instructions || 'Add a note for the rider or restaurant'} + + + + )} - - Delivery Fee - ₹{totals.deliveryFee} + */} + + {/* ── Tip Your Delivery Partner ──────────────────────────── */} + {/* + + + + 100% of the tip goes directly to your delivery partner. They + work hard so your food arrives hot! + + + {TIP_OPTIONS.map(amount => ( + + setSelectedTip(prev => (prev === amount ? null : amount)) + } + > + {selectedTip === amount && ( + + )} + + ₹{amount} + + + ))} + + {selectedTip !== null && ( + setSelectedTip(null)} + > + ✕ Remove tip + + )} - - Platform Fee - ₹{totals.platformFee} - - {totals.discount > 0 && ( + */} + + {/* ── Bill Details ───────────────────────────────────────── */} + + + - - Discount - - - -₹{totals.discount} - + Item Total + ₹{totals.subtotal} - )} - - Total - ₹{totals.total} - + + + Delivery Fee + + + ₹{totals.deliveryFee} + + + {billExpanded && ( + + + Delivery fee helps cover your delivery partner's costs. GST as + applicable is included in the item total. + + + )} + + + Platform Fee + ₹{totals.platformFee} + + + {tipAmount > 0 && ( + + Delivery Tip 💛 + ₹{tipAmount} + + )} + + {totals.discount > 0 && ( + + + Coupon Discount + + + − ₹{totals.discount} + + + )} + + + + + + To Pay + {totals.discount > 0 && ( + + Saved ₹{totals.discount} + + )} + + ₹{grandTotal} + + + + + {/* ── Cancellation Policy ────────────────────────────────── */} + + 🛈 + + Orders cannot be cancelled once packed for delivery. In case of + unexpected delays, a refund will be provided, if applicable. + + + + + + {/* ── Bottom Panel ───────────────────────────────────────────── */} + + + + {itemCount} {itemCount === 1 ? 'item' : 'items'} + + + ₹{grandTotal} {totals.discount > 0 && ( - - 🎉 - - You're saving ₹{totals.discount} on this order - + + saved ₹{totals.discount} )} - - - - - Total - ₹{totals.total} - navigation.navigate('CheckoutAddressScreen')} style={styles.checkoutButton} /> - + ); -}; +}; \ No newline at end of file diff --git a/app/features/screens/offersScreen/offersScreen.styles.ts b/app/features/screens/offersScreen/offersScreen.styles.ts index 819124f..67e3bca 100644 --- a/app/features/screens/offersScreen/offersScreen.styles.ts +++ b/app/features/screens/offersScreen/offersScreen.styles.ts @@ -1,51 +1,178 @@ import { StyleSheet } from 'react-native'; import { typography } from '@theme'; -export const getStyles = (colors: any) => StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.background, - }, - list: { - padding: 16, - }, - offerCard: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.cardBg, - borderRadius: 12, - padding: 16, - marginBottom: 12, - borderWidth: 1, - borderColor: colors.border, - }, - offerInfo: { - flex: 1, - }, - offerCode: { - fontSize: typography.fontSize.lg, - fontWeight: typography.fontWeight.bold, - color: colors.primary, - marginBottom: 4, - }, - offerDescription: { - fontSize: typography.fontSize.sm, - color: colors.text, - marginBottom: 4, - }, - offerExpiry: { - fontSize: typography.fontSize.xs, - color: colors.textSecondary, - }, - copyButton: { - paddingHorizontal: 20, - paddingVertical: 10, - borderRadius: 8, - backgroundColor: colors.primary, - }, - copyButtonText: { - fontSize: typography.fontSize.sm, - fontWeight: typography.fontWeight.semibold, - color: '#FFFFFF', - }, -}); +export const getStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background, + }, + list: { + padding: 16, + paddingBottom: 40, + }, + offerCard: { + backgroundColor: colors.cardBg, + borderRadius: 16, + marginBottom: 16, + borderWidth: 1, + borderColor: colors.border, + overflow: 'hidden', + elevation: 2, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.05, + shadowRadius: 8, + }, + cardHeaderRow: { + flexDirection: 'row', + padding: 16, + alignItems: 'flex-start', + }, + iconWrap: { + width: 48, + height: 48, + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + iconEmoji: { + fontSize: 22, + }, + offerInfo: { + flex: 1, + paddingRight: 8, + }, + badgeContainer: { + alignSelf: 'flex-start', + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 6, + marginBottom: 6, + }, + badgeText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + letterSpacing: 0.4, + textTransform: 'uppercase', + }, + offerTitle: { + fontSize: typography.fontSize.md, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 4, + lineHeight: 22, + }, + offerDescription: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + lineHeight: 18, + }, + copyButton: { + paddingHorizontal: 16, + paddingVertical: 9, + borderRadius: 8, + backgroundColor: colors.primary, + alignSelf: 'flex-start', + marginTop: 2, + }, + copyButtonSuccess: { + backgroundColor: '#2E7D32', + }, + copyButtonText: { + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + color: '#FFFFFF', + }, + cardFooter: { + flexDirection: 'row', + flexWrap: 'wrap', + paddingHorizontal: 16, + paddingBottom: 12, + paddingTop: 10, + borderTopWidth: 1, + borderTopColor: colors.border, + backgroundColor: colors.inputBg || '#FAFAFA', + }, + metaChip: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.background, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + borderWidth: 1, + borderColor: colors.border, + marginRight: 8, + marginBottom: 4, + }, + metaChipText: { + fontSize: typography.fontSize.xs, + color: colors.textSecondary, + fontWeight: typography.fontWeight.medium, + }, + emptyWrap: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 60, + paddingHorizontal: 24, + }, + emptyIconWrap: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: colors.inputBg || '#F5F5F5', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 16, + }, + emptyEmoji: { + fontSize: 36, + }, + emptyTitle: { + fontSize: typography.fontSize.lg, + fontWeight: typography.fontWeight.bold, + color: colors.text, + marginBottom: 8, + textAlign: 'center', + }, + emptySubtitle: { + fontSize: typography.fontSize.sm, + color: colors.textSecondary, + textAlign: 'center', + lineHeight: 20, + }, + skeletonCard: { + height: 110, + backgroundColor: colors.cardBg, + borderRadius: 16, + marginBottom: 16, + padding: 16, + borderWidth: 1, + borderColor: colors.border, + justifyContent: 'space-between', + }, + skeletonLineShort: { + height: 16, + width: '40%', + backgroundColor: colors.border, + borderRadius: 8, + opacity: 0.5, + }, + skeletonLineLong: { + height: 14, + width: '80%', + backgroundColor: colors.border, + borderRadius: 7, + opacity: 0.5, + }, + skeletonLineSub: { + height: 12, + width: '60%', + backgroundColor: colors.border, + borderRadius: 6, + opacity: 0.5, + }, + }); + diff --git a/app/features/screens/offersScreen/offersScreen.tsx b/app/features/screens/offersScreen/offersScreen.tsx index 45d2e1b..2c4dda3 100644 --- a/app/features/screens/offersScreen/offersScreen.tsx +++ b/app/features/screens/offersScreen/offersScreen.tsx @@ -1,63 +1,224 @@ -import React from 'react'; -import { View, Text, FlatList, TouchableOpacity } from 'react-native'; +import React, { useState, useCallback } from 'react'; +import { + View, + Text, + FlatList, + TouchableOpacity, + StatusBar, + ToastAndroid, + Platform, +} from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; import { getStyles } from './offersScreen.styles'; import { Header } from '@components'; import { useAppTheme } from '@theme'; -import { useAppSelector } from '@store'; +import { useAppDispatch, useAppSelector, getAllOffersThunk } from '@store'; +import { Promotion, PromotionType } from '@interfaces'; -// const OFFERS = [ -// { -// id: '1', -// code: 'WELCOME50', -// description: '50% off on your first order', -// expiry: '30 Jul 2026', -// }, -// { -// id: '2', -// code: 'FLAT20', -// description: 'Flat ₹20 off on orders above ₹199', -// expiry: '15 Aug 2026', -// }, -// { -// id: '3', -// code: 'FREEDEL', -// description: 'Free delivery on orders above ₹299', -// expiry: '31 Jul 2026', -// }, -// ]; +const getPromoVisuals = (type: PromotionType) => { + switch (type) { + case 'FREE_DELIVERY': + return { + emoji: '🛵', + bgColor: '#E8F5E9', + badgeBg: '#C8E6C9', + textColor: '#1B5E20', + }; + case 'PERCENTAGE_DISCOUNT': + return { + emoji: '🏷️', + bgColor: '#FFF3E0', + badgeBg: '#FFE0B2', + textColor: '#E65100', + }; + case 'FLAT_DISCOUNT': + return { + emoji: '💰', + bgColor: '#E3F2FD', + badgeBg: '#BBDEFB', + textColor: '#0D47A1', + }; + case 'BUY_ONE_GET_ONE': + return { + emoji: '🎁', + bgColor: '#F3E5F5', + badgeBg: '#E1BEE7', + textColor: '#4A148C', + }; + default: + return { + emoji: '🎉', + bgColor: '#F5F5F5', + badgeBg: '#E0E0E0', + textColor: '#333333', + }; + } +}; export const OffersScreen: React.FC = () => { - const { colors } = useAppTheme(); + const { colors, isDarkMode } = useAppTheme(); const styles = getStyles(colors); - const { offers } = useAppSelector(state => state.offer); - // console.log(offers); + const dispatch = useAppDispatch(); + const { offers, isLoading } = useAppSelector(state => state.offer); + + const [copiedId, setCopiedId] = useState(null); + + useFocusEffect( + useCallback(() => { + dispatch(getAllOffersThunk()); + }, [dispatch]), + ); + + // const handleCopy = (item: Promotion) => { + // setCopiedId(item.id); + // const textToCopy = item.badgeText || item.title; + // if (Platform.OS === 'android') { + // ToastAndroid.show(`Copied "${textToCopy}"`, ToastAndroid.SHORT); + // } + // setTimeout(() => { + // setCopiedId(prev => (prev === item.id ? null : prev)); + // }, 2000); + // }; + + const renderOfferCard = ({ item }: { item: Promotion }) => { + const visuals = getPromoVisuals(item.promotionType); + const isCopied = copiedId === item.id; + + const hasFooter = + (item.minimumOrderAmount && item.minimumOrderAmount > 0) || + (item.maximumDiscount && item.maximumDiscount > 0) || + (item.applicableCategories && item.applicableCategories.length > 0) || + (item.applicableProducts && item.applicableProducts.length > 0); + + return ( + + {/* Main Card Header */} + + + {visuals.emoji} + + + + {!!item.badgeText && ( + + + {item.badgeText} + + + )} + {item.title} + {!!item.description && ( + {item.description} + )} + + + handleCopy(item)} + onPress={() => { }} + activeOpacity={0.8} + > + + {isCopied ? 'Copied ✓' : 'Copy'} + + + + + {/* Dynamic Meta Details Footer (Only if real data exists) */} + {hasFooter && ( + + {!!item.minimumOrderAmount && item.minimumOrderAmount > 0 && ( + + + Min order ₹{item.minimumOrderAmount} + + + )} + + {!!item.maximumDiscount && item.maximumDiscount > 0 && ( + + + Max discount ₹{item.maximumDiscount} + + + )} + + {!!item.applicableCategories?.length && ( + + + {item.applicableCategories.length}{' '} + {item.applicableCategories.length === 1 + ? 'category' + : 'categories'} + + + )} + + {!!item.applicableProducts?.length && ( + + + {item.applicableProducts.length}{' '} + {item.applicableProducts.length === 1 ? 'item' : 'items'} + + + )} + + )} + + ); + }; + + const renderSkeleton = () => ( + + {[1, 2, 3].map(key => ( + + + + + + ))} + + ); + + const renderEmpty = () => ( + + + 🏷️ + + No Offers Available + + There are currently no active promotions.{'\n'}Check back later for + exciting discounts and deals! + + + ); return ( -
- item.id} - renderItem={({ item }) => ( - - - {item?.title} - {item?.description} - {/* - Expires: {new Date(item?.).toLocaleDateString()} - */} - - {}} - activeOpacity={0.7} - > - Copy - - - )} - contentContainerStyle={styles.list} + +
+ + {isLoading && (!offers || offers.length === 0) ? ( + renderSkeleton() + ) : ( + item.id} + renderItem={renderOfferCard} + ListEmptyComponent={renderEmpty} + contentContainerStyle={styles.list} + showsVerticalScrollIndicator={false} + /> + )} ); }; + diff --git a/app/interfaces/offers.ts b/app/interfaces/offers.ts index ccbc726..e58d38e 100644 --- a/app/interfaces/offers.ts +++ b/app/interfaces/offers.ts @@ -1,6 +1,4 @@ -export interface PromotionResponse { - promotions: Promotion[]; -} +export type PromotionResponse = Promotion[]; export interface Promotion { id: string; diff --git a/app/store/commonreducers/offer/reducer.ts b/app/store/commonreducers/offer/reducer.ts index 0fb1920..31d3d45 100644 --- a/app/store/commonreducers/offer/reducer.ts +++ b/app/store/commonreducers/offer/reducer.ts @@ -1,14 +1,14 @@ -import { Promotion } from '@interfaces'; +import { PromotionResponse } from '@interfaces'; import { getAllOffersThunk } from './thunk'; import { createReducer } from '@reduxjs/toolkit'; export interface OfferState { - offers: Promotion[]; + offers: PromotionResponse | null; isLoading: boolean; error: string | null; } const initialState: OfferState = { - offers: [], + offers: null, isLoading: false, error: null, }; @@ -16,7 +16,7 @@ const initialState: OfferState = { export const offerReducer = createReducer(initialState, builder => { builder .addCase(getAllOffersThunk.fulfilled, (state, action) => { - state.offers = action.payload?.promotions; + state.offers = action.payload; state.isLoading = false; }) .addCase(getAllOffersThunk.pending, state => {