@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,220 @@
1
+ import React, { useMemo } from 'react';
2
+ import { StyleSheet, FlatList, Image } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaParticipantProps, PublicPlayerProps } from '@bettoredge/types';
6
+ import type { CalcuttaPresence } from '../../hooks/useCalcuttaSocket';
7
+ import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
8
+ import { formatCurrency } from '../../helpers/formatting';
9
+ import { showResults } from '../../helpers/lifecycleState';
10
+
11
+ interface SealedBidPlayersTabProps {
12
+ participants: CalcuttaParticipantProps[];
13
+ players: Record<string, PublicPlayerProps>;
14
+ player_id?: string;
15
+ presence: CalcuttaPresence;
16
+ market_type: string;
17
+ lifecycleState: CalcuttaLifecycleState;
18
+ socketConnected?: boolean;
19
+ listHeader?: React.ReactNode;
20
+ }
21
+
22
+ export const SealedBidPlayersTab: React.FC<SealedBidPlayersTabProps> = ({
23
+ participants,
24
+ players,
25
+ player_id,
26
+ presence,
27
+ market_type,
28
+ lifecycleState,
29
+ socketConnected,
30
+ listHeader,
31
+ }) => {
32
+ const { theme } = useTheme();
33
+ const isResultsPhase = showResults(lifecycleState);
34
+ const showOnlineStatus = socketConnected && !isResultsPhase;
35
+
36
+ const onlineIds = useMemo(
37
+ () => new Set(presence.players.map(p => p.player_id)),
38
+ [presence.players],
39
+ );
40
+
41
+ const onlineCount = useMemo(
42
+ () => participants.filter(p => onlineIds.has(p.player_id)).length,
43
+ [participants, onlineIds],
44
+ );
45
+
46
+ // Sort by rank/winnings in results phase
47
+ const sortedParticipants = useMemo(() => {
48
+ if (!isResultsPhase) return participants;
49
+ return [...participants].sort((a, b) => {
50
+ if (a.place && b.place) return a.place - b.place;
51
+ if (a.place) return -1;
52
+ if (b.place) return 1;
53
+ return (b.total_winnings || 0) - (a.total_winnings || 0);
54
+ });
55
+ }, [participants, isResultsPhase]);
56
+
57
+ if (participants.length === 0) {
58
+ return (
59
+ <View variant="transparent" style={styles.container}>
60
+ <>{listHeader}</>
61
+ <View variant="transparent" style={styles.emptyContainer}>
62
+ <Ionicons name="people-outline" size={40} color={theme.colors.text.tertiary} />
63
+ <Text variant="body" color="tertiary" style={{ marginTop: 12 }}>No players yet</Text>
64
+ </View>
65
+ </View>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <View variant="transparent" style={styles.container}>
71
+ <FlatList
72
+ data={sortedParticipants}
73
+ keyExtractor={p => p.calcutta_participant_id}
74
+ contentContainerStyle={styles.list}
75
+ ListHeaderComponent={
76
+ <View variant="transparent">
77
+ <>{listHeader}</>
78
+ <View variant="transparent" style={styles.headerRow}>
79
+ <Text variant="caption" color="secondary">
80
+ {showOnlineStatus
81
+ ? `${onlineCount} of ${participants.length} player${participants.length !== 1 ? 's' : ''} online`
82
+ : `${participants.length} player${participants.length !== 1 ? 's' : ''}`
83
+ }
84
+ </Text>
85
+ </View>
86
+ </View>
87
+ }
88
+ renderItem={({ item: participant, index }) => {
89
+ const player = players[participant.player_id];
90
+ const isOnline = onlineIds.has(participant.player_id);
91
+ const isMe = participant.player_id === player_id;
92
+ const username = player?.username || player?.show_name || `Player ${participant.player_id.slice(0, 6)}`;
93
+ const profilePic = player?.profile_pic;
94
+
95
+ return (
96
+ <View variant="transparent" style={[styles.playerRow, { borderColor: theme.colors.border.subtle }]}>
97
+ {/* Rank in results phase */}
98
+ {isResultsPhase && (
99
+ <View variant="transparent" style={styles.rankCol}>
100
+ {(participant.place ?? index + 1) <= 3 ? (
101
+ <Ionicons
102
+ name="trophy"
103
+ size={14}
104
+ color={(participant.place ?? index + 1) === 1 ? '#FFD700' : (participant.place ?? index + 1) === 2 ? '#C0C0C0' : '#CD7F32'}
105
+ />
106
+ ) : (
107
+ <Text variant="caption" color="tertiary">{participant.place ?? index + 1}</Text>
108
+ )}
109
+ </View>
110
+ )}
111
+
112
+ {/* Avatar */}
113
+ <View variant="transparent" style={styles.avatarWrap}>
114
+ {profilePic ? (
115
+ <Image source={{ uri: profilePic }} style={styles.avatar} />
116
+ ) : (
117
+ <View variant="transparent" style={[styles.avatar, { backgroundColor: theme.colors.primary.subtle }]}>
118
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
119
+ {username.charAt(0).toUpperCase()}
120
+ </Text>
121
+ </View>
122
+ )}
123
+ {showOnlineStatus && isOnline && (
124
+ <View variant="transparent" style={[styles.onlineDot, { backgroundColor: '#10B981', borderColor: theme.colors.surface.base }]} />
125
+ )}
126
+ </View>
127
+
128
+ {/* Name */}
129
+ <View variant="transparent" style={{ flex: 1 }}>
130
+ <View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center' }}>
131
+ <Text variant="body" bold={isMe} numberOfLines={1}>{username}</Text>
132
+ {isMe && (
133
+ <View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
134
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 9 }}>YOU</Text>
135
+ </View>
136
+ )}
137
+ </View>
138
+ {isResultsPhase && (
139
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>
140
+ {participant.items_owned > 0
141
+ ? `${participant.items_owned} item${participant.items_owned !== 1 ? 's' : ''} · ${formatCurrency(participant.total_spent, market_type)} spent`
142
+ : 'No items'}
143
+ </Text>
144
+ )}
145
+ </View>
146
+
147
+ {/* Winnings in results phase */}
148
+ {isResultsPhase && participant.total_winnings > 0 && (
149
+ <View variant="transparent" style={{ alignItems: 'flex-end' }}>
150
+ <Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>Won</Text>
151
+ <Text variant="body" bold style={{ color: theme.colors.status.success }}>
152
+ {formatCurrency(participant.total_winnings, market_type)}
153
+ </Text>
154
+ </View>
155
+ )}
156
+ </View>
157
+ );
158
+ }}
159
+ />
160
+ </View>
161
+ );
162
+ };
163
+
164
+ const styles = StyleSheet.create({
165
+ container: {
166
+ flex: 1,
167
+ },
168
+ emptyContainer: {
169
+ flex: 1,
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ padding: 40,
173
+ },
174
+ headerRow: {
175
+ paddingHorizontal: 16,
176
+ paddingVertical: 10,
177
+ },
178
+ list: {
179
+ paddingBottom: 40,
180
+ },
181
+ rankCol: {
182
+ width: 28,
183
+ alignItems: 'center',
184
+ marginRight: 4,
185
+ },
186
+ playerRow: {
187
+ flexDirection: 'row',
188
+ alignItems: 'center',
189
+ paddingHorizontal: 16,
190
+ paddingVertical: 10,
191
+ borderBottomWidth: 1,
192
+ },
193
+ avatarWrap: {
194
+ marginRight: 12,
195
+ position: 'relative',
196
+ },
197
+ avatar: {
198
+ width: 40,
199
+ height: 40,
200
+ borderRadius: 20,
201
+ alignItems: 'center',
202
+ justifyContent: 'center',
203
+ overflow: 'hidden',
204
+ },
205
+ onlineDot: {
206
+ position: 'absolute',
207
+ bottom: 0,
208
+ right: 0,
209
+ width: 12,
210
+ height: 12,
211
+ borderRadius: 6,
212
+ borderWidth: 2,
213
+ },
214
+ youBadge: {
215
+ paddingHorizontal: 6,
216
+ paddingVertical: 2,
217
+ borderRadius: 6,
218
+ marginLeft: 6,
219
+ },
220
+ });
@@ -0,0 +1,415 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { StyleSheet, TouchableOpacity } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaCompetitionProps, CalcuttaEscrowProps, CalcuttaBidProps, CalcuttaAuctionItemProps } from '@bettoredge/types';
6
+ import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
7
+ import { formatCurrency } from '../../helpers/formatting';
8
+
9
+ interface SealedBidStatusBarProps {
10
+ competition: CalcuttaCompetitionProps;
11
+ escrow?: CalcuttaEscrowProps;
12
+ my_bids: CalcuttaBidProps[];
13
+ items: CalcuttaAuctionItemProps[];
14
+ totalItems: number;
15
+ market_type: string;
16
+ player_id?: string;
17
+ lifecycleState: CalcuttaLifecycleState;
18
+ auctionExpired?: boolean;
19
+ isParticipant?: boolean;
20
+ onEscrowTap: () => void;
21
+ onLeave?: () => void;
22
+ leaving?: boolean;
23
+ }
24
+
25
+ const getTimerColor = (msLeft: number) => {
26
+ if (msLeft > 3600000) return '#10B981';
27
+ if (msLeft > 600000) return '#F59E0B';
28
+ return '#EF4444';
29
+ };
30
+
31
+ export const SealedBidStatusBar: React.FC<SealedBidStatusBarProps> = ({
32
+ competition,
33
+ escrow,
34
+ my_bids,
35
+ items,
36
+ totalItems,
37
+ market_type,
38
+ player_id,
39
+ lifecycleState,
40
+ auctionExpired,
41
+ isParticipant,
42
+ onEscrowTap,
43
+ onLeave,
44
+ leaving,
45
+ }) => {
46
+ const { theme } = useTheme();
47
+ const [timeLeft, setTimeLeft] = useState('');
48
+ const [msLeft, setMsLeft] = useState(Infinity);
49
+ const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
50
+
51
+ const timerTarget = lifecycleState === 'auctioning'
52
+ ? competition.auction_end_datetime
53
+ : lifecycleState === 'scheduled'
54
+ ? competition.auction_start_datetime
55
+ : undefined;
56
+
57
+ useEffect(() => {
58
+ if (!timerTarget) {
59
+ setTimeLeft('');
60
+ setMsLeft(0);
61
+ return;
62
+ }
63
+
64
+ const update = () => {
65
+ const end = new Date(timerTarget).getTime();
66
+ const diff = end - Date.now();
67
+ setMsLeft(diff);
68
+
69
+ if (diff <= 0) {
70
+ setTimeLeft(lifecycleState === 'auctioning' ? 'Closed' : 'Starting...');
71
+ if (timerRef.current) clearInterval(timerRef.current);
72
+ return;
73
+ }
74
+
75
+ const d = Math.floor(diff / 86400000);
76
+ const h = Math.floor((diff % 86400000) / 3600000);
77
+ const m = Math.floor((diff % 3600000) / 60000);
78
+ const s = Math.floor((diff % 60000) / 1000);
79
+
80
+ if (d > 0) setTimeLeft(`${d}d ${h}h`);
81
+ else if (h > 0) setTimeLeft(`${h}h ${m}m`);
82
+ else setTimeLeft(`${m}:${s.toString().padStart(2, '0')}`);
83
+ };
84
+
85
+ update();
86
+ timerRef.current = setInterval(update, 1000);
87
+ return () => { if (timerRef.current) clearInterval(timerRef.current); };
88
+ }, [timerTarget, lifecycleState]);
89
+
90
+ const escrowBalance = Number(escrow?.escrow_balance ?? 0);
91
+ const committed = Number(escrow?.committed_balance ?? 0);
92
+ const total = escrowBalance + committed;
93
+ // All items I purchased (active + eliminated)
94
+ const myPurchasedItems = items.filter(i => i.winning_player_id === player_id);
95
+
96
+ // ============================================
97
+ // PENDING
98
+ // ============================================
99
+ if (lifecycleState === 'pending') {
100
+ return (
101
+ <View variant="transparent" style={[styles.container, { borderColor: theme.colors.border.subtle }]}>
102
+
103
+ <View variant="transparent" style={styles.cells}>
104
+ <View variant="transparent" style={styles.cell}>
105
+ <Ionicons name="lock-closed-outline" size={14} color="#F59E0B" />
106
+ <Text variant="caption" bold style={{ color: '#F59E0B', marginLeft: 4 }}>
107
+ Coming Soon
108
+ </Text>
109
+ </View>
110
+ <View variant="transparent" style={styles.cell}>
111
+ <Text variant="caption" color="secondary">Items: {totalItems}</Text>
112
+ </View>
113
+ <View variant="transparent" style={styles.cell} />
114
+ </View>
115
+ </View>
116
+ );
117
+ }
118
+
119
+ // ============================================
120
+ // SCHEDULED
121
+ // ============================================
122
+ if (lifecycleState === 'scheduled') {
123
+ return (
124
+ <View variant="transparent" style={[styles.container, { borderColor: theme.colors.border.subtle }]}>
125
+
126
+ <View variant="transparent" style={styles.cells}>
127
+ <View variant="transparent" style={styles.cell}>
128
+ <Ionicons name="calendar-outline" size={14} color="#3B82F6" />
129
+ <Text variant="caption" bold style={{ color: '#3B82F6', marginLeft: 4 }}>
130
+ {timeLeft ? `Starts ${timeLeft}` : 'Scheduled'}
131
+ </Text>
132
+ </View>
133
+ <TouchableOpacity style={styles.cell} onPress={onEscrowTap} activeOpacity={0.7}>
134
+ <Ionicons name="wallet-outline" size={14} color={theme.colors.text.secondary} />
135
+ <Text variant="caption" bold style={{ marginLeft: 4 }}>
136
+ {formatCurrency(escrowBalance, market_type)}
137
+ </Text>
138
+ </TouchableOpacity>
139
+ <View variant="transparent" style={styles.cell}>
140
+ <Ionicons name="people-outline" size={14} color={theme.colors.text.secondary} />
141
+ <Text variant="caption" bold style={{ marginLeft: 4 }}>
142
+ {items.length > 0 ? `${totalItems} items` : '0'}
143
+ </Text>
144
+ </View>
145
+ </View>
146
+ </View>
147
+ );
148
+ }
149
+
150
+ // ============================================
151
+ // AUCTIONING — big KPI-style bar
152
+ // ============================================
153
+ if (lifecycleState === 'auctioning') {
154
+ const activeBids = my_bids.filter(b => b.bid_status === 'active').length;
155
+ const timerColor = auctionExpired ? '#F59E0B' : getTimerColor(msLeft);
156
+
157
+ // Safe auction metrics (no bid amount leaks)
158
+ const totalBidsPlaced = items.reduce((s, i) => s + (i.bid_count ?? 0), 0);
159
+ const itemsWithBids = items.filter(i => (i.bid_count ?? 0) > 0).length;
160
+ const minPot = competition.min_bid * totalItems;
161
+ const myTotalBid = my_bids
162
+ .filter(b => b.bid_status === 'active')
163
+ .reduce((s, b) => s + Number(b.bid_amount), 0);
164
+
165
+ return (
166
+ <View variant="transparent" style={[styles.auctionContainer, { borderColor: theme.colors.border.subtle }]}>
167
+
168
+ {/* Timer row */}
169
+ <View variant="transparent" style={styles.timerRow}>
170
+ <Ionicons name={auctionExpired ? 'hourglass-outline' : 'timer-outline'} size={18} color={timerColor} />
171
+ <Text variant="h3" bold style={{ color: timerColor, marginLeft: 6 }}>
172
+ {auctionExpired ? 'Closing...' : timeLeft}
173
+ </Text>
174
+ </View>
175
+
176
+ {/* Auction Activity (safe metrics only) */}
177
+ <View variant="transparent" style={[styles.potTracker, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
178
+ <View variant="transparent" style={styles.potHeader}>
179
+ <Ionicons name="pulse-outline" size={16} color={theme.colors.primary.default} />
180
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 6 }}>Auction Activity</Text>
181
+ </View>
182
+ <View variant="transparent" style={styles.potRow}>
183
+ <View variant="transparent" style={styles.potItem}>
184
+ <Text variant="h3" bold>{itemsWithBids}<Text variant="caption" color="tertiary">/{totalItems}</Text></Text>
185
+ <Text variant="caption" color="tertiary">Items Bid</Text>
186
+ </View>
187
+ <View variant="transparent" style={[styles.potDivider, { backgroundColor: theme.colors.border.subtle }]} />
188
+ <View variant="transparent" style={styles.potItem}>
189
+ <Text variant="h3" bold>{totalBidsPlaced}</Text>
190
+ <Text variant="caption" color="tertiary">Total Bids</Text>
191
+ </View>
192
+ <View variant="transparent" style={[styles.potDivider, { backgroundColor: theme.colors.border.subtle }]} />
193
+ <View variant="transparent" style={styles.potItem}>
194
+ <Text variant="h3" bold>{formatCurrency(minPot, market_type)}</Text>
195
+ <Text variant="caption" color="tertiary">Min Pot</Text>
196
+ </View>
197
+ </View>
198
+ </View>
199
+
200
+ {/* My Escrow KPIs — only for participants */}
201
+ {isParticipant && (
202
+ <View variant="transparent" style={styles.kpiRow}>
203
+ <TouchableOpacity
204
+ style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}
205
+ onPress={onEscrowTap}
206
+ activeOpacity={0.7}
207
+ >
208
+ <Ionicons name="wallet-outline" size={16} color={theme.colors.primary.default} />
209
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Available</Text>
210
+ <Text variant="body" bold>{formatCurrency(escrowBalance, market_type)}</Text>
211
+ </TouchableOpacity>
212
+
213
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
214
+ <Ionicons name="pricetag-outline" size={16} color={theme.colors.text.secondary} />
215
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>My Bids</Text>
216
+ <Text variant="body" bold>{formatCurrency(myTotalBid, market_type)}</Text>
217
+ </View>
218
+
219
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
220
+ <Ionicons name="pricetag-outline" size={16} color={theme.colors.text.secondary} />
221
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>My Items</Text>
222
+ <Text variant="body" bold>{activeBids}<Text variant="caption" color="tertiary">/{totalItems}</Text></Text>
223
+ </View>
224
+ </View>
225
+ )}
226
+
227
+ {/* Transfer actions — only for participants */}
228
+ {isParticipant && <View variant="transparent" style={styles.transferRow}>
229
+ <TouchableOpacity
230
+ style={[styles.transferBtn, { backgroundColor: theme.colors.primary.default }]}
231
+ onPress={onEscrowTap}
232
+ activeOpacity={0.7}
233
+ >
234
+ <Ionicons name="arrow-down-outline" size={16} color="#FFFFFF" />
235
+ <Text variant="caption" bold style={{ color: '#FFFFFF', marginLeft: 4 }}>
236
+ Transfer In
237
+ </Text>
238
+ </TouchableOpacity>
239
+ <TouchableOpacity
240
+ style={[styles.transferBtn, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderWidth: 1 }]}
241
+ onPress={onEscrowTap}
242
+ activeOpacity={0.7}
243
+ >
244
+ <Ionicons name="arrow-up-outline" size={16} color={theme.colors.text.secondary} />
245
+ <Text variant="caption" bold style={{ color: theme.colors.text.secondary, marginLeft: 4 }}>
246
+ Transfer Out
247
+ </Text>
248
+ </TouchableOpacity>
249
+ </View>}
250
+
251
+ {/* Leave button — only when: not expired, no active bids, no escrow balance */}
252
+ {onLeave && !auctionExpired && activeBids === 0 && escrowBalance === 0 && committed === 0 && (
253
+ <TouchableOpacity
254
+ style={styles.leaveBtn}
255
+ onPress={onLeave}
256
+ disabled={leaving}
257
+ activeOpacity={0.7}
258
+ >
259
+ <Ionicons name="exit-outline" size={16} color="#EF4444" />
260
+ <Text variant="caption" bold style={{ color: '#EF4444', marginLeft: 6 }}>
261
+ {leaving ? 'Leaving...' : 'Leave Auction'}
262
+ </Text>
263
+ </TouchableOpacity>
264
+ )}
265
+ </View>
266
+ );
267
+ }
268
+
269
+ // ============================================
270
+ // TOURNAMENT
271
+ // ============================================
272
+ if (lifecycleState === 'tournament') {
273
+ const totalPurchased = myPurchasedItems.reduce((s, i) => s + Number(i.winning_bid), 0);
274
+ const stillAlive = myPurchasedItems.filter(i => i.status !== 'eliminated').length;
275
+
276
+ return (
277
+ <View variant="transparent" style={[styles.auctionContainer, { borderColor: theme.colors.border.subtle }]}>
278
+
279
+ <View variant="transparent" style={styles.kpiRow}>
280
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
281
+ <Ionicons name="ellipse" size={8} color="#10B981" />
282
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Status</Text>
283
+ <Text variant="body" bold style={{ color: '#8B5CF6' }}>In Progress</Text>
284
+ </View>
285
+
286
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
287
+ <Ionicons name="cart-outline" size={16} color={theme.colors.primary.default} />
288
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Purchased</Text>
289
+ <Text variant="body" bold>{myPurchasedItems.length} <Text variant="caption" color="tertiary">({stillAlive} alive)</Text></Text>
290
+ </View>
291
+
292
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
293
+ <Ionicons name="cash-outline" size={16} color={theme.colors.text.secondary} />
294
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Spent</Text>
295
+ <Text variant="body" bold>{formatCurrency(totalPurchased, market_type)}</Text>
296
+ </View>
297
+ </View>
298
+ </View>
299
+ );
300
+ }
301
+
302
+ // ============================================
303
+ // COMPLETED
304
+ // ============================================
305
+ const totalPurchased = myPurchasedItems.reduce((s, i) => s + Number(i.winning_bid), 0);
306
+ return (
307
+ <View variant="transparent" style={[styles.auctionContainer, { borderColor: theme.colors.border.subtle }]}>
308
+
309
+ <View variant="transparent" style={styles.kpiRow}>
310
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
311
+ <Ionicons name="checkmark-circle" size={16} color={theme.colors.status.success} />
312
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Status</Text>
313
+ <Text variant="body" bold style={{ color: theme.colors.status.success }}>Completed</Text>
314
+ </View>
315
+
316
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
317
+ <Ionicons name="cart-outline" size={16} color={theme.colors.primary.default} />
318
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Purchased</Text>
319
+ <Text variant="body" bold>{myPurchasedItems.length}</Text>
320
+ </View>
321
+
322
+ <View variant="transparent" style={[styles.kpiCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
323
+ <Ionicons name="cash-outline" size={16} color={theme.colors.text.secondary} />
324
+ <Text variant="caption" color="tertiary" style={{ marginTop: 2 }}>Spent</Text>
325
+ <Text variant="body" bold>{formatCurrency(totalPurchased, market_type)}</Text>
326
+ </View>
327
+ </View>
328
+ </View>
329
+ );
330
+ };
331
+
332
+ const styles = StyleSheet.create({
333
+ // Compact container for pending/scheduled
334
+ container: {
335
+ borderBottomWidth: 1,
336
+ paddingTop: 8,
337
+ paddingHorizontal: 12,
338
+ paddingBottom: 6,
339
+ },
340
+ cells: {
341
+ flexDirection: 'row',
342
+ justifyContent: 'space-between',
343
+ },
344
+ cell: {
345
+ flexDirection: 'row',
346
+ alignItems: 'center',
347
+ },
348
+ // Full KPI container for auctioning/tournament/completed
349
+ auctionContainer: {
350
+ borderBottomWidth: 1,
351
+ paddingHorizontal: 12,
352
+ paddingVertical: 10,
353
+ },
354
+ timerRow: {
355
+ flexDirection: 'row',
356
+ alignItems: 'center',
357
+ justifyContent: 'center',
358
+ marginBottom: 10,
359
+ },
360
+ potTracker: {
361
+ borderRadius: 12,
362
+ borderWidth: 1,
363
+ padding: 12,
364
+ marginBottom: 10,
365
+ },
366
+ potHeader: {
367
+ flexDirection: 'row',
368
+ alignItems: 'center',
369
+ marginBottom: 10,
370
+ },
371
+ potRow: {
372
+ flexDirection: 'row',
373
+ alignItems: 'center',
374
+ },
375
+ potItem: {
376
+ flex: 1,
377
+ alignItems: 'center',
378
+ },
379
+ potDivider: {
380
+ width: 1,
381
+ height: 32,
382
+ },
383
+ kpiRow: {
384
+ flexDirection: 'row',
385
+ gap: 8,
386
+ },
387
+ kpiCard: {
388
+ flex: 1,
389
+ borderRadius: 10,
390
+ borderWidth: 1,
391
+ paddingVertical: 10,
392
+ paddingHorizontal: 8,
393
+ alignItems: 'center',
394
+ },
395
+ transferRow: {
396
+ flexDirection: 'row',
397
+ gap: 8,
398
+ marginTop: 10,
399
+ },
400
+ transferBtn: {
401
+ flex: 1,
402
+ flexDirection: 'row',
403
+ alignItems: 'center',
404
+ justifyContent: 'center',
405
+ height: 36,
406
+ borderRadius: 8,
407
+ },
408
+ leaveBtn: {
409
+ flexDirection: 'row',
410
+ alignItems: 'center',
411
+ justifyContent: 'center',
412
+ marginTop: 10,
413
+ paddingVertical: 8,
414
+ },
415
+ });