@bettoredge/calcutta 0.2.0

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.
Files changed (38) hide show
  1. package/package.json +46 -0
  2. package/src/components/CalcuttaAuction.tsx +453 -0
  3. package/src/components/CalcuttaAuctionItem.tsx +292 -0
  4. package/src/components/CalcuttaBidInput.tsx +214 -0
  5. package/src/components/CalcuttaCard.tsx +131 -0
  6. package/src/components/CalcuttaDetail.tsx +377 -0
  7. package/src/components/CalcuttaEscrow.tsx +464 -0
  8. package/src/components/CalcuttaItemResults.tsx +207 -0
  9. package/src/components/CalcuttaLeaderboard.tsx +179 -0
  10. package/src/components/CalcuttaPayoutPreview.tsx +194 -0
  11. package/src/components/CalcuttaRoundResults.tsx +250 -0
  12. package/src/components/CalcuttaTemplateSelector.tsx +124 -0
  13. package/src/components/sealed/AuctionResultsModal.tsx +165 -0
  14. package/src/components/sealed/EscrowBottomSheet.tsx +185 -0
  15. package/src/components/sealed/SealedBidAuction.tsx +541 -0
  16. package/src/components/sealed/SealedBidHeader.tsx +116 -0
  17. package/src/components/sealed/SealedBidInfoTab.tsx +247 -0
  18. package/src/components/sealed/SealedBidItemCard.tsx +385 -0
  19. package/src/components/sealed/SealedBidItemsTab.tsx +235 -0
  20. package/src/components/sealed/SealedBidMyBidsTab.tsx +512 -0
  21. package/src/components/sealed/SealedBidPlayersTab.tsx +220 -0
  22. package/src/components/sealed/SealedBidStatusBar.tsx +415 -0
  23. package/src/components/sealed/SealedBidTabBar.tsx +172 -0
  24. package/src/helpers/formatting.ts +56 -0
  25. package/src/helpers/lifecycleState.ts +71 -0
  26. package/src/helpers/payout.ts +39 -0
  27. package/src/helpers/validation.ts +64 -0
  28. package/src/hooks/useCalcuttaAuction.ts +164 -0
  29. package/src/hooks/useCalcuttaBid.ts +43 -0
  30. package/src/hooks/useCalcuttaCompetition.ts +63 -0
  31. package/src/hooks/useCalcuttaEscrow.ts +52 -0
  32. package/src/hooks/useCalcuttaItemImages.ts +79 -0
  33. package/src/hooks/useCalcuttaPlayers.ts +46 -0
  34. package/src/hooks/useCalcuttaResults.ts +58 -0
  35. package/src/hooks/useCalcuttaSocket.ts +131 -0
  36. package/src/hooks/useCalcuttaTemplates.ts +36 -0
  37. package/src/index.ts +74 -0
  38. package/src/types.ts +31 -0
@@ -0,0 +1,247 @@
1
+ import React from 'react';
2
+ import { StyleSheet, TouchableOpacity, Image, ScrollView, Share } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type {
6
+ CalcuttaCompetitionProps,
7
+ CalcuttaRoundProps,
8
+ CalcuttaPayoutRuleProps,
9
+ CalcuttaEscrowProps,
10
+ } from '@bettoredge/types';
11
+ import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
12
+ import { canManageEscrow } from '../../helpers/lifecycleState';
13
+ import { formatCurrency } from '../../helpers/formatting';
14
+ import { CalcuttaEscrow } from '../CalcuttaEscrow';
15
+
16
+ interface SealedBidInfoTabProps {
17
+ competition: CalcuttaCompetitionProps;
18
+ rounds: CalcuttaRoundProps[];
19
+ payout_rules: CalcuttaPayoutRuleProps[];
20
+ escrow?: CalcuttaEscrowProps;
21
+ market_type: string;
22
+ player_balance?: number;
23
+ onDepositFunds?: (amount: number) => void;
24
+ onEscrowUpdate: () => void;
25
+ lifecycleState: CalcuttaLifecycleState;
26
+ }
27
+
28
+ export const SealedBidInfoTab: React.FC<SealedBidInfoTabProps> = ({
29
+ competition,
30
+ rounds,
31
+ payout_rules,
32
+ escrow,
33
+ market_type,
34
+ player_balance,
35
+ onDepositFunds,
36
+ onEscrowUpdate,
37
+ lifecycleState,
38
+ }) => {
39
+ const { theme } = useTheme();
40
+ const totalPot = Number(competition.total_pot) || 0;
41
+ const escrowInteractive = canManageEscrow(lifecycleState);
42
+ const showEscrow = lifecycleState !== 'pending';
43
+
44
+ const handleCopyCode = () => {
45
+ // Copy is handled by the share flow or parent component
46
+ };
47
+
48
+ const handleShare = async () => {
49
+ try {
50
+ const msg = competition.competition_code
51
+ ? `Join my Calcutta auction "${competition.competition_name}"! Code: ${competition.competition_code}`
52
+ : `Check out the Calcutta auction "${competition.competition_name}" on BettorEdge!`;
53
+ await Share.share({ message: msg });
54
+ } catch {}
55
+ };
56
+
57
+ return (
58
+ <ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
59
+ {/* Hero image */}
60
+ {competition.image?.url ? (
61
+ <Image source={{ uri: competition.image.url }} style={styles.heroImage} resizeMode="cover" />
62
+ ) : (
63
+ <View variant="transparent" style={[styles.heroPlaceholder, { backgroundColor: theme.colors.surface.elevated }]}>
64
+ <Ionicons name="hammer-outline" size={48} color={theme.colors.text.tertiary} />
65
+ </View>
66
+ )}
67
+
68
+ {/* Name + description */}
69
+ <View variant="transparent" style={styles.section}>
70
+ <Text variant="h3" bold>{competition.competition_name}</Text>
71
+ {competition.competition_description ? (
72
+ <Text variant="body" color="secondary" style={{ marginTop: 6 }}>{competition.competition_description}</Text>
73
+ ) : null}
74
+ </View>
75
+
76
+ {/* Payout structure */}
77
+ {payout_rules.length > 0 && (
78
+ <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
79
+ <Text variant="body" bold style={{ marginBottom: 8 }}>Payout Structure</Text>
80
+ {payout_rules.map((rule, i) => {
81
+ const round = rounds.find(r => r.round_number === rule.round_number);
82
+ const pct = Number(rule.payout_pct);
83
+ const amount = totalPot > 0 ? (pct / 100) * totalPot : 0;
84
+ return (
85
+ <View
86
+ key={rule.calcutta_payout_rule_id || `rule-${i}`}
87
+ variant="transparent"
88
+ style={[styles.payoutRow, i < payout_rules.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}
89
+ >
90
+ <Text variant="body" style={{ flex: 1 }}>
91
+ {rule.description || round?.round_name || `Round ${rule.round_number}`}
92
+ </Text>
93
+ <Text variant="body" bold>{pct}%</Text>
94
+ {totalPot > 0 && (
95
+ <Text variant="caption" color="tertiary" style={{ marginLeft: 8, minWidth: 60, textAlign: 'right' }}>
96
+ {formatCurrency(amount, market_type)}
97
+ </Text>
98
+ )}
99
+ </View>
100
+ );
101
+ })}
102
+ </View>
103
+ )}
104
+
105
+ {/* Rules card */}
106
+ {(competition.min_bid > 0 || competition.bid_increment > 0 || competition.max_escrow || competition.min_spend_pct) && (
107
+ <View variant="transparent" style={[styles.rulesCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
108
+ <Text variant="body" bold style={{ marginBottom: 8 }}>Rules</Text>
109
+ {competition.min_bid > 0 && (
110
+ <Text variant="caption" color="secondary" style={styles.ruleItem}>
111
+ Min Bid: {formatCurrency(competition.min_bid, market_type)}
112
+ </Text>
113
+ )}
114
+ {competition.bid_increment > 0 && (
115
+ <Text variant="caption" color="secondary" style={styles.ruleItem}>
116
+ Bid Increment: {formatCurrency(competition.bid_increment, market_type)}
117
+ </Text>
118
+ )}
119
+ {competition.max_escrow != null && Number(competition.max_escrow) > 0 && (
120
+ <Text variant="caption" color="secondary" style={styles.ruleItem}>
121
+ Budget Cap: {formatCurrency(Number(competition.max_escrow), market_type)}
122
+ </Text>
123
+ )}
124
+ {competition.min_spend_pct != null && Number(competition.min_spend_pct) > 0 && (
125
+ <Text variant="caption" color="secondary" style={styles.ruleItem}>
126
+ Min Spend: {competition.min_spend_pct}% of deposited funds
127
+ </Text>
128
+ )}
129
+ </View>
130
+ )}
131
+
132
+ {/* Rounds */}
133
+ {rounds.length > 0 && (
134
+ <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
135
+ <Text variant="body" bold style={{ marginBottom: 8 }}>Rounds</Text>
136
+ {rounds.map(r => (
137
+ <View key={r.calcutta_round_id} variant="transparent" style={styles.roundRow}>
138
+ <Text variant="caption" color="tertiary" style={{ minWidth: 20 }}>{r.round_number}</Text>
139
+ <Text variant="body" style={{ marginLeft: 8 }}>{r.round_name}</Text>
140
+ </View>
141
+ ))}
142
+ </View>
143
+ )}
144
+
145
+ {/* Competition code */}
146
+ {competition.competition_code && (
147
+ <View variant="transparent" style={styles.section}>
148
+ <TouchableOpacity
149
+ onPress={handleCopyCode}
150
+ style={[styles.codeRow, { backgroundColor: theme.colors.primary.subtle }]}
151
+ activeOpacity={0.7}
152
+ >
153
+ <Ionicons name="key-outline" size={14} color={theme.colors.primary.default} />
154
+ <Text variant="body" bold style={{ color: theme.colors.primary.default, marginLeft: 6, flex: 1 }}>
155
+ {competition.competition_code}
156
+ </Text>
157
+ <Ionicons name="copy-outline" size={16} color={theme.colors.primary.default} />
158
+ </TouchableOpacity>
159
+ </View>
160
+ )}
161
+
162
+ {/* Share button */}
163
+ <View variant="transparent" style={styles.section}>
164
+ <TouchableOpacity
165
+ onPress={handleShare}
166
+ style={[styles.shareBtn, { borderColor: theme.colors.border.subtle }]}
167
+ activeOpacity={0.7}
168
+ >
169
+ <Ionicons name="share-outline" size={16} color={theme.colors.text.secondary} />
170
+ <Text variant="caption" bold color="secondary" style={{ marginLeft: 6 }}>Share</Text>
171
+ </TouchableOpacity>
172
+ </View>
173
+
174
+ {/* Escrow management — hidden in pending, read-only in tournament/completed */}
175
+ {showEscrow && (
176
+ <View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
177
+ <Text variant="body" bold style={{ marginBottom: 8 }}>Escrow</Text>
178
+ <CalcuttaEscrow
179
+ calcutta_competition_id={competition.calcutta_competition_id}
180
+ escrow={escrow}
181
+ market_type={market_type}
182
+ max_escrow={competition.max_escrow}
183
+ player_balance={player_balance}
184
+ onDepositFunds={onDepositFunds}
185
+ onUpdate={onEscrowUpdate}
186
+ readOnly={!escrowInteractive}
187
+ />
188
+ </View>
189
+ )}
190
+ </ScrollView>
191
+ );
192
+ };
193
+
194
+ const styles = StyleSheet.create({
195
+ content: {
196
+ paddingBottom: 60,
197
+ },
198
+ heroImage: {
199
+ width: '100%',
200
+ height: 180,
201
+ },
202
+ heroPlaceholder: {
203
+ width: '100%',
204
+ height: 120,
205
+ alignItems: 'center',
206
+ justifyContent: 'center',
207
+ },
208
+ section: {
209
+ padding: 16,
210
+ borderTopWidth: 1,
211
+ },
212
+ payoutRow: {
213
+ flexDirection: 'row',
214
+ alignItems: 'center',
215
+ paddingVertical: 10,
216
+ },
217
+ rulesCard: {
218
+ margin: 16,
219
+ marginTop: 0,
220
+ padding: 14,
221
+ borderRadius: 10,
222
+ borderWidth: 1,
223
+ },
224
+ ruleItem: {
225
+ marginBottom: 4,
226
+ },
227
+ roundRow: {
228
+ flexDirection: 'row',
229
+ alignItems: 'center',
230
+ paddingVertical: 6,
231
+ },
232
+ codeRow: {
233
+ flexDirection: 'row',
234
+ alignItems: 'center',
235
+ paddingHorizontal: 14,
236
+ paddingVertical: 10,
237
+ borderRadius: 10,
238
+ },
239
+ shareBtn: {
240
+ flexDirection: 'row',
241
+ alignItems: 'center',
242
+ justifyContent: 'center',
243
+ paddingVertical: 10,
244
+ borderRadius: 10,
245
+ borderWidth: 1,
246
+ },
247
+ });
@@ -0,0 +1,385 @@
1
+ import React, { useState } from 'react';
2
+ import { StyleSheet, TouchableOpacity, Image } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaAuctionItemProps, CalcuttaBidProps } from '@bettoredge/types';
6
+ import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
7
+ import { formatCurrency } from '../../helpers/formatting';
8
+ import { CalcuttaBidInput } from '../CalcuttaBidInput';
9
+
10
+ interface SealedBidItemCardProps {
11
+ item: CalcuttaAuctionItemProps;
12
+ my_bid?: CalcuttaBidProps;
13
+ min_bid: number;
14
+ bid_increment: number;
15
+ escrow_balance: number;
16
+ market_type: string;
17
+ player_id?: string;
18
+ lifecycleState: CalcuttaLifecycleState;
19
+ itemImage?: { url: string };
20
+ onPlaceBid: (amount: number) => void;
21
+ bidLoading: boolean;
22
+ disabled?: boolean;
23
+ }
24
+
25
+ const QUICK_AMOUNTS = [5, 10, 25, 50];
26
+
27
+ export const SealedBidItemCard: React.FC<SealedBidItemCardProps> = ({
28
+ item,
29
+ my_bid,
30
+ min_bid,
31
+ bid_increment,
32
+ escrow_balance,
33
+ market_type,
34
+ player_id,
35
+ lifecycleState,
36
+ itemImage,
37
+ onPlaceBid,
38
+ bidLoading,
39
+ disabled,
40
+ }) => {
41
+ const { theme } = useTheme();
42
+ const [expanded, setExpanded] = useState(false);
43
+
44
+ const hasBid = my_bid && my_bid.bid_status === 'active';
45
+ const isSold = item.status === 'sold';
46
+ const iOwnThis = isSold && item.winning_player_id === player_id;
47
+ const canBidNow = lifecycleState === 'auctioning' && (item.status === 'active' || item.status === 'pending') && !disabled;
48
+
49
+ // Resolve image: prefer fetched team/athlete image, fall back to stored item_image
50
+ const resolvedImageUrl = itemImage?.url || item.item_image?.url;
51
+
52
+ // ============================================
53
+ // PENDING / SCHEDULED — Browse mode (info only)
54
+ // ============================================
55
+ if (lifecycleState === 'pending' || lifecycleState === 'scheduled') {
56
+ return (
57
+ <View
58
+ variant="transparent"
59
+ style={[
60
+ styles.card,
61
+ {
62
+ backgroundColor: theme.colors.surface.elevated,
63
+ borderColor: theme.colors.border.subtle,
64
+ },
65
+ ]}
66
+ >
67
+ <View style={styles.row}>
68
+ <View variant="transparent" style={[styles.iconWrap, { backgroundColor: theme.colors.surface.base, overflow: 'hidden' }]}>
69
+ {resolvedImageUrl ? (
70
+ <Image source={{ uri: resolvedImageUrl }} style={styles.itemImage} resizeMode="contain" />
71
+ ) : (
72
+ <Ionicons
73
+ name={item.item_type === 'team' ? 'people-outline' : 'person-outline'}
74
+ size={16}
75
+ color={theme.colors.text.tertiary}
76
+ />
77
+ )}
78
+ </View>
79
+ <View variant="transparent" style={styles.info}>
80
+ <View variant="transparent" style={styles.nameRow}>
81
+ <Text variant="body" bold numberOfLines={1} style={{ flex: 1 }}>
82
+ {item.item_name}
83
+ </Text>
84
+ {item.seed != null && (
85
+ <Text variant="caption" color="tertiary" style={{ marginLeft: 6 }}>
86
+ #{item.seed}
87
+ </Text>
88
+ )}
89
+ </View>
90
+ {item.item_type && (
91
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2, fontSize: 11 }}>
92
+ {item.item_type === 'team' ? 'Team' : 'Player'}
93
+ </Text>
94
+ )}
95
+ </View>
96
+ </View>
97
+ </View>
98
+ );
99
+ }
100
+
101
+ // ============================================
102
+ // TOURNAMENT / COMPLETED — Results view
103
+ // ============================================
104
+ if (lifecycleState === 'tournament' || lifecycleState === 'completed') {
105
+ const didBid = my_bid != null;
106
+ return (
107
+ <View
108
+ variant="transparent"
109
+ style={[
110
+ styles.card,
111
+ {
112
+ backgroundColor: theme.colors.surface.elevated,
113
+ borderColor: iOwnThis
114
+ ? theme.colors.status.success + '80'
115
+ : theme.colors.border.subtle,
116
+ },
117
+ !isSold && { opacity: 0.5 },
118
+ ]}
119
+ >
120
+ <View style={styles.row}>
121
+ <View variant="transparent" style={[styles.iconWrap, {
122
+ backgroundColor: iOwnThis
123
+ ? theme.colors.status.successSubtle
124
+ : theme.colors.surface.base,
125
+ overflow: 'hidden',
126
+ }]}>
127
+ {resolvedImageUrl ? (
128
+ <Image source={{ uri: resolvedImageUrl }} style={styles.itemImage} resizeMode="contain" />
129
+ ) : iOwnThis ? (
130
+ <Ionicons name="trophy" size={18} color="#F59E0B" />
131
+ ) : isSold ? (
132
+ <Ionicons name="close-circle" size={18} color={theme.colors.text.tertiary} />
133
+ ) : (
134
+ <Ionicons
135
+ name={item.item_type === 'team' ? 'people-outline' : 'person-outline'}
136
+ size={16}
137
+ color={theme.colors.text.tertiary}
138
+ />
139
+ )}
140
+ </View>
141
+ <View variant="transparent" style={styles.info}>
142
+ <View variant="transparent" style={styles.nameRow}>
143
+ <Text variant="body" bold numberOfLines={1} style={{ flex: 1 }}>
144
+ {item.item_name}
145
+ </Text>
146
+ {item.seed != null && (
147
+ <Text variant="caption" color="tertiary" style={{ marginLeft: 6 }}>
148
+ #{item.seed}
149
+ </Text>
150
+ )}
151
+ </View>
152
+ <View variant="transparent" style={styles.metaRow}>
153
+ {iOwnThis && (
154
+ <View variant="transparent" style={[styles.wonBadge, { backgroundColor: theme.colors.status.successSubtle }]}>
155
+ <Text variant="caption" bold style={{ color: theme.colors.status.success, fontSize: 10 }}>
156
+ YOU WON
157
+ </Text>
158
+ </View>
159
+ )}
160
+ {isSold && !iOwnThis && didBid && (
161
+ <View variant="transparent" style={[styles.wonBadge, { backgroundColor: theme.colors.status.error + '15' }]}>
162
+ <Text variant="caption" bold style={{ color: theme.colors.status.error, fontSize: 10 }}>
163
+ OUTBID
164
+ </Text>
165
+ </View>
166
+ )}
167
+ {!isSold && (
168
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>Unsold</Text>
169
+ )}
170
+ </View>
171
+ </View>
172
+ <View variant="transparent" style={styles.bidCol}>
173
+ {isSold ? (
174
+ <>
175
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>
176
+ {iOwnThis ? 'Paid' : 'Sold for'}
177
+ </Text>
178
+ <Text variant="body" bold style={{ color: iOwnThis ? theme.colors.status.success : theme.colors.text.primary }}>
179
+ {formatCurrency(item.winning_bid, market_type)}
180
+ </Text>
181
+ </>
182
+ ) : (
183
+ <Text variant="caption" color="tertiary">No bids</Text>
184
+ )}
185
+ </View>
186
+ </View>
187
+ {didBid && isSold && !iOwnThis && (
188
+ <View variant="transparent" style={[styles.lostBidRow, { borderColor: theme.colors.border.subtle }]}>
189
+ <Text variant="caption" color="tertiary">Your bid: {formatCurrency(my_bid!.bid_amount, market_type)}</Text>
190
+ </View>
191
+ )}
192
+ </View>
193
+ );
194
+ }
195
+
196
+ // ============================================
197
+ // AUCTIONING — Active bid UI
198
+ // ============================================
199
+ return (
200
+ <View
201
+ variant="transparent"
202
+ style={[
203
+ styles.card,
204
+ {
205
+ backgroundColor: theme.colors.surface.elevated,
206
+ borderColor: hasBid ? theme.colors.status.success + '60' : theme.colors.border.subtle,
207
+ },
208
+ ]}
209
+ >
210
+ <TouchableOpacity
211
+ style={styles.row}
212
+ activeOpacity={0.7}
213
+ onPress={() => canBidNow && setExpanded(!expanded)}
214
+ disabled={!canBidNow}
215
+ >
216
+ <View variant="transparent" style={[styles.iconWrap, { backgroundColor: theme.colors.surface.base, overflow: 'hidden' }]}>
217
+ {resolvedImageUrl ? (
218
+ <Image source={{ uri: resolvedImageUrl }} style={styles.itemImage} resizeMode="contain" />
219
+ ) : hasBid ? (
220
+ <Ionicons name="checkmark-circle" size={18} color={theme.colors.status.success} />
221
+ ) : (
222
+ <Ionicons
223
+ name={item.item_type === 'team' ? 'people-outline' : 'person-outline'}
224
+ size={16}
225
+ color={theme.colors.text.tertiary}
226
+ />
227
+ )}
228
+ </View>
229
+ <View variant="transparent" style={styles.info}>
230
+ <View variant="transparent" style={styles.nameRow}>
231
+ <Text variant="body" bold numberOfLines={1} style={{ flex: 1 }}>
232
+ {item.item_name}
233
+ </Text>
234
+ {item.seed != null && (
235
+ <Text variant="caption" color="tertiary" style={{ marginLeft: 6 }}>
236
+ #{item.seed}
237
+ </Text>
238
+ )}
239
+ </View>
240
+ <View variant="transparent" style={styles.metaRow}>
241
+ {item.bid_count != null && item.bid_count > 0 && (
242
+ <Text variant="caption" color="tertiary" style={{ fontSize: 11 }}>
243
+ {item.bid_count} bidder{item.bid_count !== 1 ? 's' : ''}
244
+ </Text>
245
+ )}
246
+ </View>
247
+ </View>
248
+ <View variant="transparent" style={styles.bidCol}>
249
+ {hasBid ? (
250
+ <>
251
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>Your bid</Text>
252
+ <Text variant="body" bold style={{ color: theme.colors.status.success }}>
253
+ {formatCurrency(my_bid!.bid_amount, market_type)}
254
+ </Text>
255
+ </>
256
+ ) : canBidNow ? (
257
+ <View variant="transparent" style={[styles.bidCta, { backgroundColor: theme.colors.primary.subtle }]}>
258
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 11 }}>
259
+ Place Bid
260
+ </Text>
261
+ </View>
262
+ ) : (
263
+ <Text variant="caption" color="tertiary" style={{ fontSize: 11 }}>No bid</Text>
264
+ )}
265
+ </View>
266
+ {canBidNow && (
267
+ <Ionicons
268
+ name={expanded ? 'chevron-up' : 'chevron-down'}
269
+ size={14}
270
+ color={theme.colors.text.tertiary}
271
+ style={{ marginLeft: 6 }}
272
+ />
273
+ )}
274
+ </TouchableOpacity>
275
+
276
+ {expanded && canBidNow && (
277
+ <View variant="transparent" style={[styles.expandedArea, { borderColor: theme.colors.border.subtle }]}>
278
+ <View variant="transparent" style={styles.quickRow}>
279
+ {QUICK_AMOUNTS.map(amt => (
280
+ <TouchableOpacity
281
+ key={amt}
282
+ style={[styles.quickBtn, { backgroundColor: theme.colors.surface.base, borderColor: theme.colors.border.subtle }]}
283
+ onPress={() => onPlaceBid(amt)}
284
+ disabled={bidLoading}
285
+ activeOpacity={0.7}
286
+ >
287
+ <Text variant="caption" bold style={{ color: theme.colors.text.primary }}>
288
+ {formatCurrency(amt, market_type)}
289
+ </Text>
290
+ </TouchableOpacity>
291
+ ))}
292
+ </View>
293
+ <CalcuttaBidInput
294
+ current_bid={item.current_bid}
295
+ min_bid={min_bid}
296
+ bid_increment={bid_increment}
297
+ auction_type="sealed_bid"
298
+ escrow_balance={escrow_balance}
299
+ existing_bid_amount={my_bid?.bid_amount}
300
+ onSubmit={onPlaceBid}
301
+ loading={bidLoading}
302
+ disabled={disabled}
303
+ />
304
+ </View>
305
+ )}
306
+ </View>
307
+ );
308
+ };
309
+
310
+ const styles = StyleSheet.create({
311
+ card: {
312
+ borderWidth: 1,
313
+ borderRadius: 10,
314
+ marginHorizontal: 12,
315
+ marginBottom: 8,
316
+ overflow: 'hidden',
317
+ },
318
+ row: {
319
+ flexDirection: 'row',
320
+ alignItems: 'center',
321
+ padding: 12,
322
+ },
323
+ iconWrap: {
324
+ width: 36,
325
+ height: 36,
326
+ borderRadius: 18,
327
+ alignItems: 'center',
328
+ justifyContent: 'center',
329
+ },
330
+ itemImage: {
331
+ width: 36,
332
+ height: 36,
333
+ borderRadius: 18,
334
+ },
335
+ info: {
336
+ flex: 1,
337
+ marginLeft: 10,
338
+ },
339
+ nameRow: {
340
+ flexDirection: 'row',
341
+ alignItems: 'center',
342
+ },
343
+ metaRow: {
344
+ flexDirection: 'row',
345
+ alignItems: 'center',
346
+ marginTop: 2,
347
+ },
348
+ wonBadge: {
349
+ paddingHorizontal: 6,
350
+ paddingVertical: 2,
351
+ borderRadius: 4,
352
+ },
353
+ bidCol: {
354
+ alignItems: 'flex-end',
355
+ marginLeft: 10,
356
+ },
357
+ bidCta: {
358
+ paddingHorizontal: 10,
359
+ paddingVertical: 5,
360
+ borderRadius: 6,
361
+ },
362
+ lostBidRow: {
363
+ paddingHorizontal: 12,
364
+ paddingBottom: 10,
365
+ borderTopWidth: 1,
366
+ paddingTop: 8,
367
+ },
368
+ expandedArea: {
369
+ borderTopWidth: 1,
370
+ paddingHorizontal: 12,
371
+ paddingBottom: 12,
372
+ },
373
+ quickRow: {
374
+ flexDirection: 'row',
375
+ paddingTop: 10,
376
+ gap: 8,
377
+ },
378
+ quickBtn: {
379
+ flex: 1,
380
+ paddingVertical: 8,
381
+ borderRadius: 6,
382
+ borderWidth: 1,
383
+ alignItems: 'center',
384
+ },
385
+ });