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 { 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,
},
});

View File

@ -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<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 } = useAppTheme();
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}>
<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}>
<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.emptySubtitle}>
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!
</Text>
<TouchableOpacity
style={styles.emptyButton}
activeOpacity={0.85}
onPress={() => navigation.goBack()}
>
<Text style={styles.emptyButtonText}>Start Ordering</Text>
<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}>
<Header title="Cart" onBack={() => navigation.goBack()} />
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<Header title="My Cart" onBack={() => navigation.goBack()} />
<ScrollView
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
<Animated.View
style={[{ flex: 1 }, { opacity: fadeAnim, transform: [{ translateY: slideAnim }] }]}
>
<View style={styles.etaBanner}>
<Text style={styles.etaIcon}>🛵</Text>
<Text style={styles.etaText}>Delivery in 2025 mins</Text>
</View>
<View style={styles.itemsCard}>
{items.map((item, index) => (
<View
key={item.id}
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>
<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>
<QuantitySelector
value={item.quantity}
onIncrement={() =>
dispatch(
updateCartItemThunk({
productId: item.productId,
quantity: item.quantity + 1,
}),
)
}
onDecrement={() => {
if (item.quantity >= 1) {
<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,
quantity: item.quantity + 1,
}),
);
)
}
}}
/>
</View>
))}
</View>
onDecrement={() => {
if (item.quantity >= 1) {
dispatch(
updateCartItemThunk({
productId: item.productId,
quantity: item.quantity - 1,
}),
);
}
}}
/>
</View>
))}
<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 style={styles.footer}>
{/* Coupon logic removed/commented out for now as requested
<Text style={styles.sectionTitle}>Apply Coupon</Text>
<View style={styles.couponCard}>
...
{/* 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>
*/}
<Text style={styles.sectionTitle}>Bill Details</Text>
<View style={styles.billCard}>
<View style={styles.feeRow}>
<Text style={styles.feeLabel}>Subtotal</Text>
<Text style={styles.feeValue}>{totals.subtotal}</Text>
{/* ── 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 style={styles.feeRow}>
<Text style={styles.feeLabel}>Delivery Fee</Text>
<Text style={styles.feeValue}>{totals.deliveryFee}</Text>
</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 style={styles.feeRow}>
<Text style={styles.feeLabel}>Platform Fee</Text>
<Text style={styles.feeValue}>{totals.platformFee}</Text>
</View>
{totals.discount > 0 && (
</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, { color: colors.primary }]}>
Discount
</Text>
<Text style={[styles.feeValue, { color: colors.primary }]}>
-{totals.discount}
</Text>
<Text style={styles.feeLabel}>Item Total</Text>
<Text style={styles.feeValue}>{totals.subtotal}</Text>
</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 && (
<View style={styles.savingsBanner}>
<Text style={styles.savingsIcon}>🎉</Text>
<Text style={styles.savingsText}>
You're saving {totals.discount} on this order
</Text>
<View style={styles.bottomSavedBadge}>
<Text style={styles.bottomSavedText}>saved {totals.discount}</Text>
</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
title="Proceed to Checkout"
title="Proceed to Checkout →"
onPress={() => navigation.navigate('CheckoutAddressScreen')}
style={styles.checkoutButton}
/>
</View>
</Animated.View>
</View>
);
};
};

View File

@ -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,
},
});

View File

@ -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<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 (
<View style={styles.container}>
<Header title="Offers" />
<FlatList
data={offers}
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}
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={colors.background}
/>
<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 File

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

View File

@ -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 => {