@bettoredge/calcutta 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/components/AuctionCompleteOverlay.tsx +306 -0
- package/src/components/AuctionCountdownOverlay.tsx +178 -0
- package/src/components/AuctionInfoChips.tsx +105 -0
- package/src/components/AuctionPausedOverlay.tsx +154 -0
- package/src/components/CalcuttaActionCard.tsx +291 -0
- package/src/components/CalcuttaAuction.tsx +677 -281
- package/src/components/CalcuttaAuctionItem.tsx +195 -106
- package/src/components/CalcuttaBidInput.tsx +130 -139
- package/src/components/CalcuttaCountdown.tsx +183 -0
- package/src/components/CalcuttaDetail.tsx +211 -144
- package/src/components/CalcuttaEscrow.tsx +49 -13
- package/src/components/CalcuttaLeaderboard.tsx +2 -2
- package/src/components/EscrowWidget.tsx +176 -0
- package/src/components/ItemSoldCelebration.tsx +288 -0
- package/src/components/SweepstakesReveal.tsx +1 -1
- package/src/components/sealed/SealedBidAuction.tsx +7 -9
- package/src/components/sealed/SealedBidHeader.tsx +3 -3
- package/src/components/sealed/SealedBidItemCard.tsx +9 -9
- package/src/components/sealed/SealedBidItemsTab.tsx +2 -1
- package/src/components/sealed/SealedBidMyBidsTab.tsx +3 -3
- package/src/components/sealed/SealedBidPlayersTab.tsx +2 -2
- package/src/components/sealed/SealedBidStatusBar.tsx +1 -1
- package/src/components/sealed/SealedBidTabBar.tsx +2 -0
- package/src/hooks/useCalcuttaAuction.ts +16 -2
- package/src/hooks/useCalcuttaEscrow.ts +5 -1
- package/src/hooks/useCalcuttaSocket.ts +80 -82
- package/src/index.ts +18 -0
|
@@ -11,7 +11,7 @@ export interface CalcuttaBidInputProps {
|
|
|
11
11
|
auction_type: 'sealed_bid' | 'live';
|
|
12
12
|
escrow_balance: number;
|
|
13
13
|
existing_bid_amount?: number;
|
|
14
|
-
onSubmit: (amount: number) => void
|
|
14
|
+
onSubmit: (amount: number) => void | Promise<void>;
|
|
15
15
|
loading: boolean;
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
}
|
|
@@ -33,136 +33,129 @@ export const CalcuttaBidInput: React.FC<CalcuttaBidInputProps> = ({
|
|
|
33
33
|
? Math.max(Number(min_bid), Number(current_bid) + Number(bid_increment))
|
|
34
34
|
: Number(min_bid);
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
const [
|
|
36
|
+
const step = Number(bid_increment) > 0 ? Number(bid_increment) : 1;
|
|
37
|
+
const available = escrow_balance;
|
|
38
|
+
const isHighestBidder = existing_bid_amount != null && existing_bid_amount >= Number(current_bid) && Number(current_bid) > 0;
|
|
39
|
+
const [raise, setRaise] = useState(0);
|
|
40
|
+
const [error, setError] = useState('');
|
|
41
|
+
const [submitting, setSubmitting] = useState(false);
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
}, [minimumAllowed, existing_bid_amount]);
|
|
43
|
+
// Total bid = minimum + raise
|
|
44
|
+
const totalBid = minimumAllowed + raise;
|
|
45
|
+
const canAfford = totalBid <= available;
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Reset raise when minimum changes (new bid came in)
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setRaise(0);
|
|
50
|
+
}, [minimumAllowed]);
|
|
48
51
|
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
setError('Enter a valid bid amount');
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
if (numericAmount < minimumAllowed) {
|
|
55
|
-
setError(`Minimum bid is ${formatCurrency(minimumAllowed)}`);
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
if (auction_type === 'live' && Number(bid_increment) > 0 && (numericAmount - minimumAllowed) % Number(bid_increment) !== 0) {
|
|
59
|
-
setError(`Bid must be in increments of ${formatCurrency(Number(bid_increment))}`);
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
const available = escrow_balance + (existing_bid_amount || 0);
|
|
63
|
-
if (numericAmount > available) {
|
|
52
|
+
const handleSubmit = async () => {
|
|
53
|
+
if (!canAfford) {
|
|
64
54
|
setError(`Insufficient escrow. Available: ${formatCurrency(available)}`);
|
|
65
|
-
return
|
|
55
|
+
return;
|
|
66
56
|
}
|
|
57
|
+
setSubmitting(true);
|
|
67
58
|
setError('');
|
|
68
|
-
|
|
59
|
+
try {
|
|
60
|
+
await onSubmit(totalBid);
|
|
61
|
+
setRaise(0);
|
|
62
|
+
} catch (e: any) {
|
|
63
|
+
setError(e?.message || 'Failed to place bid');
|
|
64
|
+
}
|
|
65
|
+
setSubmitting(false);
|
|
69
66
|
};
|
|
70
67
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const bumpUp = () => {
|
|
69
|
+
const next = raise + step;
|
|
70
|
+
if (minimumAllowed + next <= available) {
|
|
71
|
+
setRaise(next);
|
|
72
|
+
setError('');
|
|
74
73
|
}
|
|
75
74
|
};
|
|
76
75
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const newAmount = direction === 'up' ? current + step : Math.max(minimumAllowed, current - step);
|
|
81
|
-
setAmount(newAmount.toString());
|
|
76
|
+
const bumpDown = () => {
|
|
77
|
+
const next = raise - step;
|
|
78
|
+
setRaise(Math.max(0, next));
|
|
82
79
|
setError('');
|
|
83
80
|
};
|
|
84
81
|
|
|
85
82
|
return (
|
|
86
83
|
<View variant="transparent" style={styles.container}>
|
|
87
|
-
{/* Amount input row */}
|
|
88
|
-
<View variant="transparent" style={styles.inputRow}>
|
|
89
|
-
<TouchableOpacity
|
|
90
|
-
style={[styles.adjustButton, { backgroundColor: theme.colors.surface.base, borderColor: theme.colors.border.subtle }]}
|
|
91
|
-
onPress={() => adjustAmount('down')}
|
|
92
|
-
disabled={numericAmount <= minimumAllowed}
|
|
93
|
-
>
|
|
94
|
-
<Ionicons
|
|
95
|
-
name="remove"
|
|
96
|
-
size={18}
|
|
97
|
-
color={numericAmount <= minimumAllowed ? theme.colors.text.tertiary : theme.colors.text.primary}
|
|
98
|
-
/>
|
|
99
|
-
</TouchableOpacity>
|
|
100
|
-
|
|
101
|
-
<TextInput
|
|
102
|
-
style={[
|
|
103
|
-
styles.input,
|
|
104
|
-
{
|
|
105
|
-
color: theme.colors.text.primary,
|
|
106
|
-
backgroundColor: theme.colors.surface.input,
|
|
107
|
-
borderColor: error ? theme.colors.status.error : theme.colors.border.subtle,
|
|
108
|
-
},
|
|
109
|
-
]}
|
|
110
|
-
value={amount}
|
|
111
|
-
onChangeText={(text) => {
|
|
112
|
-
setAmount(text);
|
|
113
|
-
setError('');
|
|
114
|
-
}}
|
|
115
|
-
keyboardType="decimal-pad"
|
|
116
|
-
placeholder={formatCurrency(minimumAllowed)}
|
|
117
|
-
placeholderTextColor={theme.colors.text.tertiary}
|
|
118
|
-
/>
|
|
119
|
-
|
|
120
|
-
<TouchableOpacity
|
|
121
|
-
style={[styles.adjustButton, { backgroundColor: theme.colors.surface.base, borderColor: theme.colors.border.subtle }]}
|
|
122
|
-
onPress={() => adjustAmount('up')}
|
|
123
|
-
>
|
|
124
|
-
<Ionicons name="add" size={18} color={theme.colors.text.primary} />
|
|
125
|
-
</TouchableOpacity>
|
|
126
|
-
</View>
|
|
127
|
-
|
|
128
|
-
{/* Error message */}
|
|
129
|
-
{error !== '' && (
|
|
130
|
-
<Text variant="caption" style={[styles.errorText, { color: theme.colors.status.error }]}>
|
|
131
|
-
{error}
|
|
132
|
-
</Text>
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
|
-
{/* Min bid info */}
|
|
136
|
-
<Text variant="caption" color="tertiary" style={styles.minLabel}>
|
|
137
|
-
Min: {formatCurrency(minimumAllowed)}
|
|
138
|
-
{Number(bid_increment) > 0 && ` \u00B7 Increment: ${formatCurrency(Number(bid_increment))}`}
|
|
139
|
-
</Text>
|
|
140
|
-
<Text variant="caption" color="tertiary" style={styles.minLabel}>
|
|
141
|
-
Available: {formatCurrency(escrow_balance + (existing_bid_amount || 0))}
|
|
142
|
-
</Text>
|
|
143
|
-
|
|
144
|
-
{/* Submit button */}
|
|
145
84
|
{disabled ? (
|
|
146
|
-
<View variant="transparent" style={[styles.
|
|
147
|
-
<Text variant="body" bold style={{ color: theme.colors.text.tertiary }}>
|
|
148
|
-
Paused
|
|
149
|
-
</Text>
|
|
85
|
+
<View variant="transparent" style={[styles.bidButton, { backgroundColor: theme.colors.surface.base }]}>
|
|
86
|
+
<Text variant="body" bold style={{ color: theme.colors.text.tertiary }}>Paused</Text>
|
|
150
87
|
</View>
|
|
151
|
-
) : (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{loading ? (
|
|
159
|
-
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
160
|
-
) : (
|
|
161
|
-
<Text variant="body" bold style={styles.submitText}>
|
|
162
|
-
{existing_bid_amount ? 'Update Bid' : 'Place Bid'}
|
|
88
|
+
) : isHighestBidder ? (
|
|
89
|
+
<View variant="transparent" style={[styles.highestBidder, { backgroundColor: '#10B98115' }]}>
|
|
90
|
+
<Ionicons name="checkmark-circle" size={20} color="#10B981" />
|
|
91
|
+
<View variant="transparent" style={{ marginLeft: 10, flex: 1 }}>
|
|
92
|
+
<Text variant="body" bold style={{ color: '#10B981' }}>You're the highest bidder</Text>
|
|
93
|
+
<Text variant="caption" color="secondary">
|
|
94
|
+
Your bid: {formatCurrency(existing_bid_amount)} · {formatCurrency(available)} available to raise
|
|
163
95
|
</Text>
|
|
164
|
-
|
|
165
|
-
</
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
) : (
|
|
99
|
+
<>
|
|
100
|
+
{/* Raise adjuster row */}
|
|
101
|
+
<View variant="transparent" style={styles.raiseRow}>
|
|
102
|
+
<TouchableOpacity
|
|
103
|
+
style={[styles.adjustBtn, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, opacity: raise <= 0 ? 0.4 : 1 }]}
|
|
104
|
+
onPress={bumpDown}
|
|
105
|
+
disabled={raise <= 0}
|
|
106
|
+
activeOpacity={0.7}
|
|
107
|
+
>
|
|
108
|
+
<Ionicons name="remove" size={20} color={theme.colors.text.primary} />
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
|
|
111
|
+
<View variant="transparent" style={styles.raiseDisplay}>
|
|
112
|
+
{raise > 0 ? (
|
|
113
|
+
<Text variant="caption" color="secondary">+{formatCurrency(raise)} more</Text>
|
|
114
|
+
) : (
|
|
115
|
+
<Text variant="caption" color="tertiary">Minimum bid</Text>
|
|
116
|
+
)}
|
|
117
|
+
<Text variant="h3" bold>{formatCurrency(totalBid)}</Text>
|
|
118
|
+
</View>
|
|
119
|
+
|
|
120
|
+
<TouchableOpacity
|
|
121
|
+
style={[styles.adjustBtn, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, opacity: (minimumAllowed + raise + step > available) ? 0.4 : 1 }]}
|
|
122
|
+
onPress={bumpUp}
|
|
123
|
+
disabled={minimumAllowed + raise + step > available}
|
|
124
|
+
activeOpacity={0.7}
|
|
125
|
+
>
|
|
126
|
+
<Ionicons name="add" size={20} color={theme.colors.text.primary} />
|
|
127
|
+
</TouchableOpacity>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
{/* Bid button */}
|
|
131
|
+
<TouchableOpacity
|
|
132
|
+
style={[styles.bidButton, { backgroundColor: canAfford ? theme.colors.primary.default : theme.colors.surface.elevated }]}
|
|
133
|
+
onPress={handleSubmit}
|
|
134
|
+
disabled={submitting || !canAfford}
|
|
135
|
+
activeOpacity={0.7}
|
|
136
|
+
>
|
|
137
|
+
{submitting ? (
|
|
138
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
139
|
+
) : (
|
|
140
|
+
<Text variant="body" bold style={{ color: canAfford ? '#FFFFFF' : theme.colors.text.tertiary }}>
|
|
141
|
+
{existing_bid_amount ? 'Raise to' : 'Place Bid'} {formatCurrency(totalBid)}
|
|
142
|
+
</Text>
|
|
143
|
+
)}
|
|
144
|
+
</TouchableOpacity>
|
|
145
|
+
|
|
146
|
+
{/* Available */}
|
|
147
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 4, textAlign: 'center' }}>
|
|
148
|
+
{formatCurrency(available)} available
|
|
149
|
+
</Text>
|
|
150
|
+
</>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{/* Error */}
|
|
154
|
+
{error !== '' && (
|
|
155
|
+
<View variant="transparent" style={[styles.errorRow, { backgroundColor: theme.colors.status.error + '10' }]}>
|
|
156
|
+
<Ionicons name="alert-circle" size={14} color={theme.colors.status.error} />
|
|
157
|
+
<Text variant="caption" style={{ color: theme.colors.status.error, marginLeft: 6, flex: 1 }}>{error}</Text>
|
|
158
|
+
</View>
|
|
166
159
|
)}
|
|
167
160
|
</View>
|
|
168
161
|
);
|
|
@@ -170,45 +163,43 @@ export const CalcuttaBidInput: React.FC<CalcuttaBidInputProps> = ({
|
|
|
170
163
|
|
|
171
164
|
const styles = StyleSheet.create({
|
|
172
165
|
container: {
|
|
173
|
-
paddingTop:
|
|
166
|
+
paddingTop: 8,
|
|
174
167
|
},
|
|
175
|
-
|
|
168
|
+
raiseRow: {
|
|
176
169
|
flexDirection: 'row',
|
|
177
170
|
alignItems: 'center',
|
|
171
|
+
marginBottom: 8,
|
|
178
172
|
},
|
|
179
|
-
|
|
180
|
-
width:
|
|
181
|
-
height:
|
|
182
|
-
borderRadius:
|
|
173
|
+
adjustBtn: {
|
|
174
|
+
width: 44,
|
|
175
|
+
height: 44,
|
|
176
|
+
borderRadius: 10,
|
|
183
177
|
borderWidth: 1,
|
|
184
178
|
alignItems: 'center',
|
|
185
179
|
justifyContent: 'center',
|
|
186
180
|
},
|
|
187
|
-
|
|
181
|
+
raiseDisplay: {
|
|
188
182
|
flex: 1,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
borderWidth: 1,
|
|
192
|
-
marginHorizontal: 8,
|
|
193
|
-
paddingHorizontal: 12,
|
|
194
|
-
fontSize: 16,
|
|
195
|
-
textAlign: 'center',
|
|
196
|
-
},
|
|
197
|
-
errorText: {
|
|
198
|
-
marginTop: 4,
|
|
199
|
-
fontSize: 11,
|
|
200
|
-
},
|
|
201
|
-
minLabel: {
|
|
202
|
-
marginTop: 4,
|
|
183
|
+
alignItems: 'center',
|
|
184
|
+
justifyContent: 'center',
|
|
203
185
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
borderRadius: 8,
|
|
186
|
+
bidButton: {
|
|
187
|
+
height: 46,
|
|
188
|
+
borderRadius: 10,
|
|
208
189
|
alignItems: 'center',
|
|
209
190
|
justifyContent: 'center',
|
|
210
191
|
},
|
|
211
|
-
|
|
212
|
-
|
|
192
|
+
highestBidder: {
|
|
193
|
+
flexDirection: 'row',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
padding: 14,
|
|
196
|
+
borderRadius: 10,
|
|
197
|
+
},
|
|
198
|
+
errorRow: {
|
|
199
|
+
flexDirection: 'row',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
marginTop: 6,
|
|
202
|
+
padding: 8,
|
|
203
|
+
borderRadius: 8,
|
|
213
204
|
},
|
|
214
205
|
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { StyleSheet, Animated } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
|
|
6
|
+
export interface CalcuttaCountdownProps {
|
|
7
|
+
targetDate: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
size?: 'compact' | 'normal';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type UrgencyTier = 'calm' | 'upcoming' | 'soon' | 'urgent' | 'critical' | 'imminent' | 'past';
|
|
13
|
+
|
|
14
|
+
const getTier = (ms: number): UrgencyTier => {
|
|
15
|
+
if (ms <= 0) return 'past';
|
|
16
|
+
if (ms < 60_000) return 'imminent'; // < 1 min
|
|
17
|
+
if (ms < 600_000) return 'critical'; // < 10 min
|
|
18
|
+
if (ms < 3_600_000) return 'urgent'; // < 1 hour
|
|
19
|
+
if (ms < 86_400_000) return 'soon'; // < 24 hours
|
|
20
|
+
if (ms < 604_800_000) return 'upcoming'; // < 7 days
|
|
21
|
+
return 'calm';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const formatRemaining = (ms: number, tier: UrgencyTier): string => {
|
|
25
|
+
if (tier === 'past') return 'Now';
|
|
26
|
+
|
|
27
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
28
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
29
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
30
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
31
|
+
const seconds = totalSeconds % 60;
|
|
32
|
+
|
|
33
|
+
switch (tier) {
|
|
34
|
+
case 'calm': {
|
|
35
|
+
const d = new Date(Date.now() + ms);
|
|
36
|
+
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
37
|
+
}
|
|
38
|
+
case 'upcoming':
|
|
39
|
+
return `${days}d ${hours}h`;
|
|
40
|
+
case 'soon':
|
|
41
|
+
return `${hours}h ${minutes}m`;
|
|
42
|
+
case 'urgent':
|
|
43
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
44
|
+
case 'critical':
|
|
45
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
46
|
+
case 'imminent':
|
|
47
|
+
return `0:${String(seconds).padStart(2, '0')}`;
|
|
48
|
+
default:
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const CalcuttaCountdown: React.FC<CalcuttaCountdownProps> = ({
|
|
54
|
+
targetDate,
|
|
55
|
+
label,
|
|
56
|
+
size = 'normal',
|
|
57
|
+
}) => {
|
|
58
|
+
const { theme } = useTheme();
|
|
59
|
+
const [remaining, setRemaining] = useState(() => new Date(targetDate).getTime() - Date.now());
|
|
60
|
+
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
61
|
+
const tier = getTier(remaining);
|
|
62
|
+
|
|
63
|
+
// Live tick for urgent tiers
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const needsTick = ['urgent', 'critical', 'imminent'].includes(tier);
|
|
66
|
+
if (!needsTick) {
|
|
67
|
+
// Update every minute for less urgent tiers
|
|
68
|
+
const interval = setInterval(() => {
|
|
69
|
+
setRemaining(new Date(targetDate).getTime() - Date.now());
|
|
70
|
+
}, 60_000);
|
|
71
|
+
return () => clearInterval(interval);
|
|
72
|
+
}
|
|
73
|
+
const interval = setInterval(() => {
|
|
74
|
+
setRemaining(new Date(targetDate).getTime() - Date.now());
|
|
75
|
+
}, 1000);
|
|
76
|
+
return () => clearInterval(interval);
|
|
77
|
+
}, [targetDate, tier]);
|
|
78
|
+
|
|
79
|
+
// Pulse animation for critical/imminent
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (tier === 'critical' || tier === 'imminent') {
|
|
82
|
+
const loop = Animated.loop(
|
|
83
|
+
Animated.sequence([
|
|
84
|
+
Animated.timing(pulseAnim, { toValue: 0.6, duration: 600, useNativeDriver: true }),
|
|
85
|
+
Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
|
|
86
|
+
])
|
|
87
|
+
);
|
|
88
|
+
loop.start();
|
|
89
|
+
return () => loop.stop();
|
|
90
|
+
} else {
|
|
91
|
+
pulseAnim.setValue(1);
|
|
92
|
+
}
|
|
93
|
+
}, [tier]);
|
|
94
|
+
|
|
95
|
+
const getTierColor = () => {
|
|
96
|
+
switch (tier) {
|
|
97
|
+
case 'calm':
|
|
98
|
+
case 'upcoming':
|
|
99
|
+
return theme.colors.text.secondary;
|
|
100
|
+
case 'soon':
|
|
101
|
+
return theme.colors.primary.default;
|
|
102
|
+
case 'urgent':
|
|
103
|
+
return '#D97706'; // amber
|
|
104
|
+
case 'critical':
|
|
105
|
+
case 'imminent':
|
|
106
|
+
return theme.colors.status.error;
|
|
107
|
+
case 'past':
|
|
108
|
+
return theme.colors.text.tertiary;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getTierBg = () => {
|
|
113
|
+
switch (tier) {
|
|
114
|
+
case 'critical':
|
|
115
|
+
case 'imminent':
|
|
116
|
+
return theme.colors.status.error + '15';
|
|
117
|
+
case 'urgent':
|
|
118
|
+
return '#D9770615';
|
|
119
|
+
default:
|
|
120
|
+
return 'transparent';
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isCompact = size === 'compact';
|
|
125
|
+
const timeColor = getTierColor();
|
|
126
|
+
const formatted = formatRemaining(remaining, tier);
|
|
127
|
+
|
|
128
|
+
if (tier === 'past') return null;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Animated.View style={[
|
|
132
|
+
styles.container,
|
|
133
|
+
isCompact && styles.containerCompact,
|
|
134
|
+
{ backgroundColor: getTierBg(), opacity: tier === 'critical' || tier === 'imminent' ? pulseAnim : 1 },
|
|
135
|
+
]}>
|
|
136
|
+
{label && (
|
|
137
|
+
<Text variant="caption" color="tertiary" style={isCompact ? styles.labelCompact : styles.label}>
|
|
138
|
+
{label}
|
|
139
|
+
</Text>
|
|
140
|
+
)}
|
|
141
|
+
<View variant="transparent" style={styles.timeRow}>
|
|
142
|
+
<Ionicons
|
|
143
|
+
name="time-outline"
|
|
144
|
+
size={isCompact ? 14 : 16}
|
|
145
|
+
color={timeColor}
|
|
146
|
+
style={{ marginRight: 4 }}
|
|
147
|
+
/>
|
|
148
|
+
<Text
|
|
149
|
+
variant={isCompact ? 'caption' : 'body'}
|
|
150
|
+
bold={tier !== 'calm' && tier !== 'upcoming'}
|
|
151
|
+
style={{ color: timeColor }}
|
|
152
|
+
>
|
|
153
|
+
{tier === 'calm' || tier === 'upcoming' ? formatted : formatted}
|
|
154
|
+
</Text>
|
|
155
|
+
</View>
|
|
156
|
+
</Animated.View>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const styles = StyleSheet.create({
|
|
161
|
+
container: {
|
|
162
|
+
borderRadius: 8,
|
|
163
|
+
paddingHorizontal: 10,
|
|
164
|
+
paddingVertical: 6,
|
|
165
|
+
alignSelf: 'flex-start',
|
|
166
|
+
},
|
|
167
|
+
containerCompact: {
|
|
168
|
+
paddingHorizontal: 8,
|
|
169
|
+
paddingVertical: 4,
|
|
170
|
+
flexDirection: 'row',
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
},
|
|
173
|
+
label: {
|
|
174
|
+
marginBottom: 2,
|
|
175
|
+
},
|
|
176
|
+
labelCompact: {
|
|
177
|
+
marginRight: 6,
|
|
178
|
+
},
|
|
179
|
+
timeRow: {
|
|
180
|
+
flexDirection: 'row',
|
|
181
|
+
alignItems: 'center',
|
|
182
|
+
},
|
|
183
|
+
});
|