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