284 lines
7.9 KiB
TypeScript
284 lines
7.9 KiB
TypeScript
import React, { memo, useMemo } from 'react';
|
|
import { StyleSheet, Text, View } from 'react-native';
|
|
import Ionicons from '@react-native-vector-icons/ionicons';
|
|
|
|
import { useTheme } from '../../../theme';
|
|
import type { CardPhysicalDelivery } from '../../../types/app';
|
|
import { formatDate } from '../../../utils/formatters';
|
|
import {
|
|
getCardDeliveryProgressIndex,
|
|
getCardDeliveryStatusLabel,
|
|
} from '../utils/cardCopy';
|
|
|
|
interface CardDeliveryStatusProps {
|
|
delivery: CardPhysicalDelivery;
|
|
compact?: boolean;
|
|
}
|
|
|
|
type DeliveryStep = {
|
|
id: string;
|
|
label: string;
|
|
};
|
|
|
|
const CardDeliveryStatusComponent = ({
|
|
delivery,
|
|
compact = false,
|
|
}: CardDeliveryStatusProps) => {
|
|
const theme = useTheme();
|
|
const statusLabel = getCardDeliveryStatusLabel(delivery);
|
|
const progressIndex = getCardDeliveryProgressIndex(delivery);
|
|
|
|
const steps = useMemo<DeliveryStep[]>(
|
|
() =>
|
|
delivery.mode === 'home'
|
|
? [
|
|
{ id: 'processing', label: 'Requested' },
|
|
{ id: 'personalizing', label: 'Prepared' },
|
|
{ id: 'shipped', label: 'In transit' },
|
|
{ id: 'delivered', label: 'Delivered' },
|
|
]
|
|
: [
|
|
{ id: 'processing', label: 'Requested' },
|
|
{ id: 'personalizing', label: 'Prepared' },
|
|
{ id: 'ready-for-pickup', label: 'At branch' },
|
|
{ id: 'delivered', label: 'Collected' },
|
|
],
|
|
[delivery.mode],
|
|
);
|
|
|
|
const accentColor =
|
|
delivery.status === 'delivered'
|
|
? theme.colors.accentStrong
|
|
: delivery.status === 'ready-for-pickup'
|
|
? theme.colors.warning
|
|
: theme.colors.primary;
|
|
const accentBackground =
|
|
delivery.status === 'delivered'
|
|
? `${theme.colors.accent}12`
|
|
: delivery.status === 'ready-for-pickup'
|
|
? `${theme.colors.warning}14`
|
|
: `${theme.colors.primary}12`;
|
|
const modeLabel = delivery.mode === 'home' ? 'Home delivery' : 'Branch pickup';
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.header}>
|
|
<View style={styles.headerCopy}>
|
|
<Text style={[styles.eyebrow, { color: theme.colors.textSecondary }]}>
|
|
Physical card delivery
|
|
</Text>
|
|
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{statusLabel}</Text>
|
|
<Text style={[styles.subtitle, { color: theme.colors.textSecondary }]}>
|
|
{delivery.status === 'delivered'
|
|
? `Completed on ${formatDate(delivery.lastUpdatedAt)}`
|
|
: `Expected by ${formatDate(delivery.estimatedDate)}`}
|
|
</Text>
|
|
</View>
|
|
<View style={[styles.badge, { backgroundColor: accentBackground }]}>
|
|
<Ionicons name="cube-outline" size={14} color={accentColor} />
|
|
<Text style={[styles.badgeText, { color: accentColor }]}>{modeLabel}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.metricsRow}>
|
|
<View style={[styles.metricTile, { backgroundColor: theme.colors.surfaceMuted }]}>
|
|
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
|
|
Tracking ID
|
|
</Text>
|
|
<Text style={[styles.metricValue, { color: theme.colors.textPrimary }]}>
|
|
{delivery.trackingId}
|
|
</Text>
|
|
</View>
|
|
<View style={[styles.metricTile, { backgroundColor: theme.colors.surfaceMuted }]}>
|
|
<Text style={[styles.metricLabel, { color: theme.colors.textSecondary }]}>
|
|
Destination
|
|
</Text>
|
|
<Text
|
|
numberOfLines={compact ? 1 : 2}
|
|
style={[styles.metricValue, { color: theme.colors.textPrimary }]}
|
|
>
|
|
{delivery.destinationLabel}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.timelineRow}>
|
|
{steps.map((step, index) => {
|
|
const isCompleted = index < progressIndex;
|
|
const isCurrent = index === progressIndex;
|
|
const activeColor = isCompleted || isCurrent ? accentColor : theme.colors.cardBorder;
|
|
const timelineDotStyle = {
|
|
backgroundColor: isCompleted || isCurrent ? activeColor : 'transparent',
|
|
borderColor: activeColor,
|
|
};
|
|
const timelineLineStyle = {
|
|
backgroundColor: index < progressIndex ? accentColor : theme.colors.cardBorder,
|
|
};
|
|
|
|
return (
|
|
<View key={step.id} style={styles.timelineItem}>
|
|
<View style={styles.timelineTopRow}>
|
|
<View style={[styles.timelineDot, timelineDotStyle]}>
|
|
{isCompleted ? (
|
|
<Ionicons name="checkmark" size={10} color={theme.colors.white} />
|
|
) : null}
|
|
</View>
|
|
{index !== steps.length - 1 ? (
|
|
<View style={[styles.timelineLine, timelineLineStyle]} />
|
|
) : null}
|
|
</View>
|
|
<Text
|
|
style={[
|
|
styles.timelineLabel,
|
|
{
|
|
color:
|
|
isCompleted || isCurrent
|
|
? theme.colors.textPrimary
|
|
: theme.colors.textMuted,
|
|
},
|
|
]}
|
|
>
|
|
{step.label}
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{!compact ? (
|
|
<View style={[styles.footerCard, { backgroundColor: theme.colors.surfaceMuted }]}>
|
|
<View style={styles.footerRow}>
|
|
<Text style={[styles.footerLabel, { color: theme.colors.textSecondary }]}>
|
|
Requested
|
|
</Text>
|
|
<Text style={[styles.footerValue, { color: theme.colors.textPrimary }]}>
|
|
{formatDate(delivery.requestedAt)}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.footerRow}>
|
|
<Text style={[styles.footerLabel, { color: theme.colors.textSecondary }]}>
|
|
Last updated
|
|
</Text>
|
|
<Text style={[styles.footerValue, { color: theme.colors.textPrimary }]}>
|
|
{formatDate(delivery.lastUpdatedAt)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
gap: 14,
|
|
},
|
|
header: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
gap: 12,
|
|
},
|
|
headerCopy: {
|
|
flex: 1,
|
|
gap: 4,
|
|
},
|
|
eyebrow: {
|
|
fontSize: 11,
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.8,
|
|
},
|
|
title: {
|
|
fontSize: 17,
|
|
fontWeight: '800',
|
|
letterSpacing: -0.2,
|
|
},
|
|
subtitle: {
|
|
fontSize: 13,
|
|
lineHeight: 18,
|
|
},
|
|
badge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
borderRadius: 999,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 8,
|
|
},
|
|
badgeText: {
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
},
|
|
metricsRow: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
},
|
|
metricTile: {
|
|
flex: 1,
|
|
borderRadius: 18,
|
|
padding: 14,
|
|
gap: 6,
|
|
},
|
|
metricLabel: {
|
|
fontSize: 11,
|
|
fontWeight: '700',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: 0.7,
|
|
},
|
|
metricValue: {
|
|
fontSize: 14,
|
|
fontWeight: '800',
|
|
},
|
|
timelineRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
},
|
|
timelineItem: {
|
|
flex: 1,
|
|
gap: 8,
|
|
},
|
|
timelineTopRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
timelineDot: {
|
|
width: 20,
|
|
height: 20,
|
|
borderRadius: 10,
|
|
borderWidth: 2,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
timelineLine: {
|
|
flex: 1,
|
|
height: 2,
|
|
marginHorizontal: 6,
|
|
},
|
|
timelineLabel: {
|
|
fontSize: 11,
|
|
fontWeight: '700',
|
|
lineHeight: 15,
|
|
},
|
|
footerCard: {
|
|
borderRadius: 18,
|
|
padding: 14,
|
|
gap: 10,
|
|
},
|
|
footerRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
},
|
|
footerLabel: {
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
},
|
|
footerValue: {
|
|
fontSize: 12,
|
|
fontWeight: '800',
|
|
},
|
|
});
|
|
|
|
export const CardDeliveryStatus = memo(CardDeliveryStatusComponent);
|