feat: implement offers screen with redux state management and interfaces

This commit is contained in:
Atanu Das 2026-07-31 18:29:12 +05:30
parent 495c1f5f6b
commit 1aed769b62
6 changed files with 1373 additions and 361 deletions

View File

@ -1,110 +1,224 @@
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { typography } from '@theme'; 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) => export const getStyles = (colors: any) =>
StyleSheet.create({ StyleSheet.create({
// ── Layout ──────────────────────────────────────────────────────────────────
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background, backgroundColor: colors.background,
}, },
list: { list: {
paddingBottom: 140, 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: { etaBanner: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.primaryMuted ?? '#E9F7EF', justifyContent: 'space-between',
marginHorizontal: 16, backgroundColor: colors.primaryMuted ?? '#E8F5EE',
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,
marginHorizontal: 16, marginHorizontal: 16,
marginTop: 14, marginTop: 14,
borderRadius: 16, 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', overflow: 'hidden',
...Platform.select({ ...cardShadow,
ios: { },
shadowColor: '#000', itemsCardHeader: {
shadowOffset: { width: 0, height: 2 }, paddingHorizontal: 16,
shadowOpacity: 0.05, paddingTop: 14,
shadowRadius: 8, paddingBottom: 6,
}, borderBottomWidth: 1,
android: { elevation: 1 }, borderBottomColor: colors.border,
}), },
itemsCardHeaderText: {
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.bold,
color: colors.textSecondary,
textTransform: 'uppercase',
letterSpacing: 0.8,
}, },
cartItem: { cartItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: 14, paddingVertical: 14,
paddingHorizontal: 14, paddingHorizontal: 16,
}, },
cartItemDivider: { cartItemDivider: {
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: colors.border, borderBottomColor: colors.border,
}, },
itemThumb: {
width: 52, // Veg indicator (left border dot)
height: 52, vegIndicator: {
borderRadius: 10, width: 14,
backgroundColor: colors.inputBg, height: 14,
borderRadius: 3,
borderWidth: 1.5,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 10,
flexShrink: 0,
},
vegDot: {
width: 6,
height: 6,
borderRadius: 3,
},
itemThumbWrap: {
marginRight: 12, marginRight: 12,
}, },
itemThumbEmoji: { itemThumb: {
fontSize: 22, width: 60,
height: 60,
borderRadius: 12,
backgroundColor: colors.inputBg,
}, },
itemInfo: { itemInfo: {
flex: 1, flex: 1,
marginRight: 10, marginRight: 8,
}, },
itemTitle: { itemTitle: {
fontSize: typography.fontSize.md, fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold, fontWeight: typography.fontWeight.semibold,
color: colors.text, color: colors.text,
marginBottom: 4, marginBottom: 6,
lineHeight: 20,
},
itemPriceRow: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 6,
}, },
itemPrice: { itemPrice: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.text,
},
itemMrp: {
fontSize: typography.fontSize.xs,
color: colors.textSecondary, 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: { addMoreRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 12, paddingVertical: 14,
marginHorizontal: 16, marginHorizontal: 14,
marginBottom: 14,
marginTop: 4, marginTop: 4,
borderWidth: 1.5, borderWidth: 1.5,
borderStyle: 'dashed', borderStyle: 'dashed',
borderColor: colors.border, borderColor: colors.primary + '55',
borderRadius: 12, borderRadius: 12,
}, },
addMoreIcon: { addMoreIcon: {
fontSize: 14, fontSize: 16,
marginRight: 6, marginRight: 6,
color: colors.primary, color: colors.primary,
fontWeight: typography.fontWeight.bold,
}, },
addMoreText: { addMoreText: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
@ -112,68 +226,100 @@ export const getStyles = (colors: any) =>
color: colors.primary, color: colors.primary,
}, },
// ---------- Footer ---------- // ── Savings Pill ───────────────────────────────────────────────────────────
footer: { savingsPill: {
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: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', 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: { couponIcon: {
fontSize: 18, fontSize: 18,
marginLeft: 10, marginRight: 8,
marginRight: 4,
}, },
couponInput: { couponInput: {
flex: 1, flex: 1,
height: 44, height: 48,
paddingHorizontal: 8, fontSize: typography.fontSize.sm,
fontSize: typography.fontSize.md,
color: colors.text, color: colors.text,
fontWeight: typography.fontWeight.medium,
}, },
applyButton: { applyButton: {
paddingHorizontal: 18, paddingHorizontal: 18,
paddingVertical: 11, paddingVertical: 9,
borderRadius: 10, borderRadius: 10,
backgroundColor: colors.primary, backgroundColor: colors.primary,
marginRight: 4, },
applyButtonDisabled: {
opacity: 0.35,
}, },
applyButtonText: { applyButtonText: {
color: '#FFFFFF', color: '#FFFFFF',
fontWeight: typography.fontWeight.semibold, fontWeight: typography.fontWeight.bold,
fontSize: typography.fontSize.sm, 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: { couponAppliedRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingVertical: 10, paddingVertical: 14,
paddingHorizontal: 12, paddingHorizontal: 14,
}, },
couponAppliedLeft: { couponAppliedLeft: {
flexDirection: 'row', flexDirection: 'row',
@ -181,16 +327,16 @@ export const getStyles = (colors: any) =>
flex: 1, flex: 1,
}, },
couponCheckBadge: { couponCheckBadge: {
width: 28, width: 36,
height: 28, height: 36,
borderRadius: 14, borderRadius: 18,
backgroundColor: colors.primaryMuted ?? '#E9F7EF', backgroundColor: colors.primary + '18',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginRight: 10, marginRight: 12,
}, },
couponCheckIcon: { couponCheckIcon: {
fontSize: 13, fontSize: 16,
}, },
couponAppliedCode: { couponAppliedCode: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
@ -200,80 +346,225 @@ export const getStyles = (colors: any) =>
couponAppliedSub: { couponAppliedSub: {
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.xs,
color: colors.primary, color: colors.primary,
marginTop: 1, marginTop: 2,
fontWeight: typography.fontWeight.medium,
},
couponRemoveBtn: {
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 8,
borderWidth: 1,
borderColor: '#D32F2F' + '44',
}, },
couponRemoveText: { 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, 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, fontWeight: typography.fontWeight.semibold,
color: '#D32F2F', color: '#D32F2F',
}, },
// Bill details // ── Bill Card ──────────────────────────────────────────────────────────────
billCard: { billCard: {
backgroundColor: colors.cardBg, backgroundColor: colors.cardBg,
borderRadius: 14, borderRadius: 16,
padding: 16, padding: 16,
...Platform.select({ ...cardShadow,
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
},
android: { elevation: 1 },
}),
}, },
feeRow: { feeRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 10, alignItems: 'center',
marginBottom: 12,
},
feeLabelRow: {
flexDirection: 'row',
alignItems: 'center',
}, },
feeLabel: { feeLabel: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
color: colors.textSecondary, color: colors.textSecondary,
}, },
infoIcon: {
fontSize: 12,
color: colors.primary,
fontWeight: typography.fontWeight.bold,
},
feeValue: { feeValue: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.medium, fontWeight: typography.fontWeight.medium,
color: colors.text, 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: { totalRow: {
borderTopWidth: 1, flexDirection: 'row',
borderTopColor: colors.border, justifyContent: 'space-between',
paddingTop: 12, alignItems: 'center',
marginTop: 4, marginTop: 4,
marginBottom: 0,
}, },
totalLabel: { totalLabel: {
fontSize: typography.fontSize.md, fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.text, color: colors.text,
}, },
totalSaved: {
fontSize: typography.fontSize.xs,
color: colors.primary,
marginTop: 2,
fontWeight: typography.fontWeight.semibold,
},
totalValue: { totalValue: {
fontSize: typography.fontSize.md, fontSize: 20,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.text, color: colors.text,
}, },
savingsBanner: {
// ── Policy Card ────────────────────────────────────────────────────────────
policyCard: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'flex-start',
backgroundColor: colors.primaryMuted ?? '#E9F7EF', marginHorizontal: 16,
borderRadius: 10, marginTop: 20,
paddingVertical: 8, marginBottom: 8,
paddingHorizontal: 12, backgroundColor: colors.inputBg,
marginTop: 14, borderRadius: 12,
paddingVertical: 12,
paddingHorizontal: 14,
}, },
savingsIcon: { policyIcon: {
fontSize: 14, fontSize: 14,
marginRight: 6, color: colors.textSecondary,
marginRight: 10,
marginTop: 1,
}, },
savingsText: { policyText: {
flex: 1,
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold, color: colors.textSecondary,
color: colors.primary, lineHeight: 18,
}, },
// ---------- Bottom panel ---------- // ── Bottom Panel ───────────────────────────────────────────────────────────
bottomPanel: { bottomPanel: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
@ -283,68 +574,107 @@ export const getStyles = (colors: any) =>
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 12, paddingTop: 14,
paddingBottom: Platform.OS === 'ios' ? 28 : 16, paddingBottom: Platform.OS === 'ios' ? 30 : 16,
backgroundColor: colors.cardBg, backgroundColor: colors.cardBg,
borderTopLeftRadius: 20, borderTopLeftRadius: 24,
borderTopRightRadius: 20, borderTopRightRadius: 24,
...Platform.select({ ...heavyShadow,
ios: { },
shadowColor: '#000', bottomTotalWrap: {
shadowOffset: { width: 0, height: -4 }, flex: 1,
shadowOpacity: 0.08,
shadowRadius: 12,
},
android: { elevation: 10 },
}),
}, },
bottomTotalWrap: {},
bottomTotalLabel: { bottomTotalLabel: {
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.xs,
color: colors.textSecondary, color: colors.textSecondary,
letterSpacing: 0.3,
marginBottom: 2,
},
bottomTotalRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
}, },
bottomTotal: { bottomTotal: {
fontSize: typography.fontSize.lg, fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.text, 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: { checkoutButton: {
flex: 1, flex: 1,
marginLeft: 16, marginLeft: 14,
}, },
// ---------- Empty state ---------- // ── Empty State ────────────────────────────────────────────────────────────
emptyWrap: { emptyWrap: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
justifyContent: '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: { emptyEmoji: {
fontSize: 56, fontSize: 52,
marginBottom: 16,
}, },
emptyTitle: { emptyTitle: {
fontSize: typography.fontSize.lg, fontSize: typography.fontSize.xl,
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
color: colors.text, color: colors.text,
marginBottom: 6, marginBottom: 10,
}, },
emptySubtitle: { emptySubtitle: {
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
color: colors.textSecondary, color: colors.textSecondary,
textAlign: 'center', textAlign: 'center',
marginBottom: 24, lineHeight: 22,
marginBottom: 30,
}, },
emptyButton: { emptyButton: {
backgroundColor: colors.primary, backgroundColor: colors.primary,
paddingHorizontal: 28, paddingHorizontal: 36,
paddingVertical: 14, paddingVertical: 15,
borderRadius: 12, borderRadius: 14,
...cardShadow,
}, },
emptyButtonText: { emptyButtonText: {
color: '#FFFFFF', color: '#FFFFFF',
fontWeight: typography.fontWeight.bold, fontWeight: typography.fontWeight.bold,
fontSize: typography.fontSize.md, 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,
}, },
}); });

View File

@ -1,5 +1,17 @@
import React, { useEffect } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, Image } from 'react-native'; import {
View,
Text,
ScrollView,
TouchableOpacity,
Image,
TextInput,
LayoutAnimation,
Platform,
UIManager,
Animated,
StatusBar,
} from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { getStyles } from './cartScreen.styles'; import { getStyles } from './cartScreen.styles';
@ -13,171 +25,555 @@ import {
import { AppStackParamList } from '../../../navigation/appStack'; import { AppStackParamList } from '../../../navigation/appStack';
import { useAppDispatch, useAppSelector } from '@store'; import { useAppDispatch, useAppSelector } from '@store';
import { getFullUrl } from '@utils'; import { getFullUrl } from '@utils';
// import { getFullUrl } from '@utils/helperr';
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>; 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 = () => { export const CartScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors, isDarkMode } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigation = useNavigation<CartScreenNavProp>(); const navigation = useNavigation<CartScreenNavProp>();
const { items, isLoading } = useAppSelector(state => state.cart); const { items, isLoading } = useAppSelector(state => state.cart);
const totals = useAppSelector(selectCartTotal); 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(() => { useEffect(() => {
dispatch(getCartThunk()); dispatch(getCartThunk());
}, [dispatch]); }, [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) { if (!isLoading && items.length === 0) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Cart" onBack={() => navigation.goBack()} /> <StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<Header title="My Cart" onBack={() => navigation.goBack()} />
<View style={styles.emptyWrap}> <View style={styles.emptyWrap}>
<Text style={styles.emptyEmoji}>🛒</Text> <View style={styles.emptyIllustration}>
<Text style={styles.emptyEmoji}>🛒</Text>
</View>
<Text style={styles.emptyTitle}>Your cart is empty</Text> <Text style={styles.emptyTitle}>Your cart is empty</Text>
<Text style={styles.emptySubtitle}> <Text style={styles.emptySubtitle}>
Looks like you haven't added anything yet. Browse providers and find Looks like you haven't added anything yet.{'\n'}Browse and find something you'll love!
something you'll love.
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={styles.emptyButton} style={styles.emptyButton}
activeOpacity={0.85} activeOpacity={0.85}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Text style={styles.emptyButtonText}>Start Ordering</Text> <Text style={styles.emptyButtonText}>Browse Menu</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</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 ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Cart" onBack={() => navigation.goBack()} /> <StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<Header title="My Cart" onBack={() => navigation.goBack()} />
<ScrollView <Animated.View
contentContainerStyle={styles.list} style={[{ flex: 1 }, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}
showsVerticalScrollIndicator={false}
> >
<View style={styles.etaBanner}> <ScrollView
<Text style={styles.etaIcon}>🛵</Text> contentContainerStyle={styles.list}
<Text style={styles.etaText}>Delivery in 2025 mins</Text> showsVerticalScrollIndicator={false}
</View> >
{/* ── ETA Banner ─────────────────────────────────────────── */}
<View style={styles.itemsCard}> <View style={styles.etaBanner}>
{items.map((item, index) => ( <View style={styles.etaLeft}>
<View <View style={styles.etaIconWrap}>
key={item.id} <Text style={styles.etaIcon}>🛵</Text>
style={[
styles.cartItem,
index < items.length - 1 && styles.cartItemDivider,
]}
>
<Image
style={styles.itemThumb}
source={{ uri: getFullUrl(item.product.imageUrl) }}
/>
<View style={styles.itemInfo}>
<Text style={styles.itemTitle} numberOfLines={2}>
{item.product.name}
</Text>
<Text style={styles.itemPrice}>{item.product.price}</Text>
</View> </View>
<QuantitySelector <View>
value={item.quantity} <Text style={styles.etaText}>Delivery in 2025 mins</Text>
onIncrement={() => <Text style={styles.etaSubtext}>Order arrives fresh & hot 🔥</Text>
dispatch( </View>
updateCartItemThunk({ </View>
productId: item.productId, <View style={styles.etaBadge}>
quantity: item.quantity + 1, <Text style={styles.etaBadgeText}>FASTEST</Text>
}), </View>
) </View>
}
onDecrement={() => { {/* ── Items Card ─────────────────────────────────────────── */}
if (item.quantity >= 1) { <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( dispatch(
updateCartItemThunk({ updateCartItemThunk({
productId: item.productId, productId: item.productId,
quantity: item.quantity - 1, quantity: item.quantity + 1,
}), }),
); )
} }
}} onDecrement={() => {
/> if (item.quantity >= 1) {
</View> dispatch(
))} updateCartItemThunk({
</View> productId: item.productId,
quantity: item.quantity - 1,
}),
);
}
}}
/>
</View>
))}
<TouchableOpacity {/* Add more items */}
style={styles.addMoreRow} <TouchableOpacity
activeOpacity={0.7} style={styles.addMoreRow}
onPress={() => navigation.goBack()} activeOpacity={0.7}
> onPress={() => navigation.goBack()}
<Text style={styles.addMoreIcon}>+</Text> >
<Text style={styles.addMoreText}>Add more items</Text> <Text style={styles.addMoreIcon}></Text>
</TouchableOpacity> <Text style={styles.addMoreText}>Add more items</Text>
</TouchableOpacity>
<View style={styles.footer}>
{/* Coupon logic removed/commented out for now as requested
<Text style={styles.sectionTitle}>Apply Coupon</Text>
<View style={styles.couponCard}>
...
</View> </View>
*/}
<Text style={styles.sectionTitle}>Bill Details</Text> {/* ── Savings if any ─────────────────────────────────────── */}
<View style={styles.billCard}> {totals.discount > 0 && (
<View style={styles.feeRow}> <SavingsPill
<Text style={styles.feeLabel}>Subtotal</Text> label={`You're saving ₹${totals.discount} on this order 🎉`}
<Text style={styles.feeValue}>{totals.subtotal}</Text> 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>
<View style={styles.feeRow}> </View> */}
<Text style={styles.feeLabel}>Delivery Fee</Text>
<Text style={styles.feeValue}>{totals.deliveryFee}</Text> {/* ── 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>
<View style={styles.feeRow}> </View> */}
<Text style={styles.feeLabel}>Platform Fee</Text>
<Text style={styles.feeValue}>{totals.platformFee}</Text> {/* ── Bill Details ───────────────────────────────────────── */}
</View> <View style={styles.section}>
{totals.discount > 0 && ( <SectionTitle label="Bill Details" styles={styles} />
<View style={styles.billCard}>
<View style={styles.feeRow}> <View style={styles.feeRow}>
<Text style={[styles.feeLabel, { color: colors.primary }]}> <Text style={styles.feeLabel}>Item Total</Text>
Discount <Text style={styles.feeValue}>{totals.subtotal}</Text>
</Text>
<Text style={[styles.feeValue, { color: colors.primary }]}>
-{totals.discount}
</Text>
</View> </View>
)}
<View style={[styles.feeRow, styles.totalRow]}>
<Text style={styles.totalLabel}>Total</Text>
<Text style={styles.totalValue}>{totals.total}</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 && ( {totals.discount > 0 && (
<View style={styles.savingsBanner}> <View style={styles.bottomSavedBadge}>
<Text style={styles.savingsIcon}>🎉</Text> <Text style={styles.bottomSavedText}>saved {totals.discount}</Text>
<Text style={styles.savingsText}>
You're saving {totals.discount} on this order
</Text>
</View> </View>
)} )}
</View> </View>
</View> </View>
</ScrollView>
<View style={styles.bottomPanel}>
<View style={styles.bottomTotalWrap}>
<Text style={styles.bottomTotalLabel}>Total</Text>
<Text style={styles.bottomTotal}>{totals.total}</Text>
</View>
<PrimaryButton <PrimaryButton
title="Proceed to Checkout" title="Proceed to Checkout →"
onPress={() => navigation.navigate('CheckoutAddressScreen')} onPress={() => navigation.navigate('CheckoutAddressScreen')}
style={styles.checkoutButton} style={styles.checkoutButton}
/> />
</View> </Animated.View>
</View> </View>
); );
}; };

View File

@ -1,51 +1,178 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { typography } from '@theme'; import { typography } from '@theme';
export const getStyles = (colors: any) => StyleSheet.create({ export const getStyles = (colors: any) =>
container: { StyleSheet.create({
flex: 1, container: {
backgroundColor: colors.background, flex: 1,
}, backgroundColor: colors.background,
list: { },
padding: 16, list: {
}, padding: 16,
offerCard: { paddingBottom: 40,
flexDirection: 'row', },
alignItems: 'center', offerCard: {
backgroundColor: colors.cardBg, backgroundColor: colors.cardBg,
borderRadius: 12, borderRadius: 16,
padding: 16, marginBottom: 16,
marginBottom: 12, borderWidth: 1,
borderWidth: 1, borderColor: colors.border,
borderColor: colors.border, overflow: 'hidden',
}, elevation: 2,
offerInfo: { shadowColor: '#000',
flex: 1, shadowOffset: { width: 0, height: 2 },
}, shadowOpacity: 0.05,
offerCode: { shadowRadius: 8,
fontSize: typography.fontSize.lg, },
fontWeight: typography.fontWeight.bold, cardHeaderRow: {
color: colors.primary, flexDirection: 'row',
marginBottom: 4, padding: 16,
}, alignItems: 'flex-start',
offerDescription: { },
fontSize: typography.fontSize.sm, iconWrap: {
color: colors.text, width: 48,
marginBottom: 4, height: 48,
}, borderRadius: 12,
offerExpiry: { justifyContent: 'center',
fontSize: typography.fontSize.xs, alignItems: 'center',
color: colors.textSecondary, marginRight: 12,
}, },
copyButton: { iconEmoji: {
paddingHorizontal: 20, fontSize: 22,
paddingVertical: 10, },
borderRadius: 8, offerInfo: {
backgroundColor: colors.primary, flex: 1,
}, paddingRight: 8,
copyButtonText: { },
fontSize: typography.fontSize.sm, badgeContainer: {
fontWeight: typography.fontWeight.semibold, alignSelf: 'flex-start',
color: '#FFFFFF', 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,
},
});

View File

@ -1,63 +1,224 @@
import React from 'react'; import React, { useState, useCallback } from 'react';
import { View, Text, FlatList, TouchableOpacity } from 'react-native'; import {
View,
Text,
FlatList,
TouchableOpacity,
StatusBar,
ToastAndroid,
Platform,
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { getStyles } from './offersScreen.styles'; import { getStyles } from './offersScreen.styles';
import { Header } from '@components'; import { Header } from '@components';
import { useAppTheme } from '@theme'; import { useAppTheme } from '@theme';
import { useAppSelector } from '@store'; import { useAppDispatch, useAppSelector, getAllOffersThunk } from '@store';
import { Promotion, PromotionType } from '@interfaces';
// const OFFERS = [ const getPromoVisuals = (type: PromotionType) => {
// { switch (type) {
// id: '1', case 'FREE_DELIVERY':
// code: 'WELCOME50', return {
// description: '50% off on your first order', emoji: '🛵',
// expiry: '30 Jul 2026', bgColor: '#E8F5E9',
// }, badgeBg: '#C8E6C9',
// { textColor: '#1B5E20',
// id: '2', };
// code: 'FLAT20', case 'PERCENTAGE_DISCOUNT':
// description: 'Flat ₹20 off on orders above ₹199', return {
// expiry: '15 Aug 2026', emoji: '🏷️',
// }, bgColor: '#FFF3E0',
// { badgeBg: '#FFE0B2',
// id: '3', textColor: '#E65100',
// code: 'FREEDEL', };
// description: 'Free delivery on orders above ₹299', case 'FLAT_DISCOUNT':
// expiry: '31 Jul 2026', 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 = () => { export const OffersScreen: React.FC = () => {
const { colors } = useAppTheme(); const { colors, isDarkMode } = useAppTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const { offers } = useAppSelector(state => state.offer); const dispatch = useAppDispatch();
// console.log(offers); const { offers, isLoading } = useAppSelector(state => state.offer);
const [copiedId, setCopiedId] = useState<string | null>(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 (
<View style={styles.offerCard}>
{/* Main Card Header */}
<View style={styles.cardHeaderRow}>
<View style={[styles.iconWrap, { backgroundColor: visuals.bgColor }]}>
<Text style={styles.iconEmoji}>{visuals.emoji}</Text>
</View>
<View style={styles.offerInfo}>
{!!item.badgeText && (
<View
style={[
styles.badgeContainer,
{ backgroundColor: visuals.badgeBg },
]}
>
<Text style={[styles.badgeText, { color: visuals.textColor }]}>
{item.badgeText}
</Text>
</View>
)}
<Text style={styles.offerTitle}>{item.title}</Text>
{!!item.description && (
<Text style={styles.offerDescription}>{item.description}</Text>
)}
</View>
<TouchableOpacity
style={[styles.copyButton, isCopied && styles.copyButtonSuccess]}
// onPress={() => handleCopy(item)}
onPress={() => { }}
activeOpacity={0.8}
>
<Text style={styles.copyButtonText}>
{isCopied ? 'Copied ✓' : 'Copy'}
</Text>
</TouchableOpacity>
</View>
{/* Dynamic Meta Details Footer (Only if real data exists) */}
{hasFooter && (
<View style={styles.cardFooter}>
{!!item.minimumOrderAmount && item.minimumOrderAmount > 0 && (
<View style={styles.metaChip}>
<Text style={styles.metaChipText}>
Min order {item.minimumOrderAmount}
</Text>
</View>
)}
{!!item.maximumDiscount && item.maximumDiscount > 0 && (
<View style={styles.metaChip}>
<Text style={styles.metaChipText}>
Max discount {item.maximumDiscount}
</Text>
</View>
)}
{!!item.applicableCategories?.length && (
<View style={styles.metaChip}>
<Text style={styles.metaChipText}>
{item.applicableCategories.length}{' '}
{item.applicableCategories.length === 1
? 'category'
: 'categories'}
</Text>
</View>
)}
{!!item.applicableProducts?.length && (
<View style={styles.metaChip}>
<Text style={styles.metaChipText}>
{item.applicableProducts.length}{' '}
{item.applicableProducts.length === 1 ? 'item' : 'items'}
</Text>
</View>
)}
</View>
)}
</View>
);
};
const renderSkeleton = () => (
<View style={styles.list}>
{[1, 2, 3].map(key => (
<View key={key} style={styles.skeletonCard}>
<View style={styles.skeletonLineShort} />
<View style={styles.skeletonLineLong} />
<View style={styles.skeletonLineSub} />
</View>
))}
</View>
);
const renderEmpty = () => (
<View style={styles.emptyWrap}>
<View style={styles.emptyIconWrap}>
<Text style={styles.emptyEmoji}>🏷</Text>
</View>
<Text style={styles.emptyTitle}>No Offers Available</Text>
<Text style={styles.emptySubtitle}>
There are currently no active promotions.{'\n'}Check back later for
exciting discounts and deals!
</Text>
</View>
);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Header title="Offers" /> <StatusBar
<FlatList barStyle={isDarkMode ? 'light-content' : 'dark-content'}
data={offers} backgroundColor={colors.background}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.offerCard}>
<View style={styles.offerInfo}>
<Text style={styles.offerCode}>{item?.title}</Text>
<Text style={styles.offerDescription}>{item?.description}</Text>
{/* <Text style={styles.offerExpiry}>
Expires: {new Date(item?.).toLocaleDateString()}
</Text> */}
</View>
<TouchableOpacity
style={styles.copyButton}
onPress={() => {}}
activeOpacity={0.7}
>
<Text style={styles.copyButtonText}>Copy</Text>
</TouchableOpacity>
</View>
)}
contentContainerStyle={styles.list}
/> />
<Header title="Offers" />
{isLoading && (!offers || offers.length === 0) ? (
renderSkeleton()
) : (
<FlatList
data={offers || []}
keyExtractor={item => item.id}
renderItem={renderOfferCard}
ListEmptyComponent={renderEmpty}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
/>
)}
</View> </View>
); );
}; };

View File

@ -1,6 +1,4 @@
export interface PromotionResponse { export type PromotionResponse = Promotion[];
promotions: Promotion[];
}
export interface Promotion { export interface Promotion {
id: string; id: string;

View File

@ -1,14 +1,14 @@
import { Promotion } from '@interfaces'; import { PromotionResponse } from '@interfaces';
import { getAllOffersThunk } from './thunk'; import { getAllOffersThunk } from './thunk';
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
export interface OfferState { export interface OfferState {
offers: Promotion[]; offers: PromotionResponse | null;
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
} }
const initialState: OfferState = { const initialState: OfferState = {
offers: [], offers: null,
isLoading: false, isLoading: false,
error: null, error: null,
}; };
@ -16,7 +16,7 @@ const initialState: OfferState = {
export const offerReducer = createReducer(initialState, builder => { export const offerReducer = createReducer(initialState, builder => {
builder builder
.addCase(getAllOffersThunk.fulfilled, (state, action) => { .addCase(getAllOffersThunk.fulfilled, (state, action) => {
state.offers = action.payload?.promotions; state.offers = action.payload;
state.isLoading = false; state.isLoading = false;
}) })
.addCase(getAllOffersThunk.pending, state => { .addCase(getAllOffersThunk.pending, state => {