feat: implement offers screen with redux state management and interfaces
This commit is contained in:
parent
495c1f5f6b
commit
1aed769b62
@ -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,
|
||||
},
|
||||
});
|
||||
@ -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 20–25 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 20–25 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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
export interface PromotionResponse {
|
||||
promotions: Promotion[];
|
||||
}
|
||||
export type PromotionResponse = Promotion[];
|
||||
|
||||
export interface Promotion {
|
||||
id: string;
|
||||
|
||||
@ -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 => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user