@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,292 @@
1
+ import React, { useState, useEffect, useRef } 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 { formatCurrency, getStatusLabel, getItemStatusColor, getBidStatusColor } from '../helpers/formatting';
7
+ import { CalcuttaBidInput } from './CalcuttaBidInput';
8
+
9
+ export interface CalcuttaAuctionItemComponentProps {
10
+ item: CalcuttaAuctionItemProps;
11
+ my_bid?: CalcuttaBidProps;
12
+ auction_type: 'sealed_bid' | 'live';
13
+ min_bid: number;
14
+ bid_increment: number;
15
+ escrow_balance: number;
16
+ onPlaceBid: (amount: number) => void;
17
+ is_active_item?: boolean;
18
+ is_paused?: boolean;
19
+ itemImage?: { url: string };
20
+ }
21
+
22
+ const getTimerColor = (secondsLeft: number, theme: any) => {
23
+ if (secondsLeft > 30) return theme.colors.status.success;
24
+ if (secondsLeft > 10) return '#F59E0B'; // amber/yellow
25
+ return theme.colors.status.error;
26
+ };
27
+
28
+ export const CalcuttaAuctionItem: React.FC<CalcuttaAuctionItemComponentProps> = ({
29
+ item,
30
+ my_bid,
31
+ auction_type,
32
+ min_bid,
33
+ bid_increment,
34
+ escrow_balance,
35
+ onPlaceBid,
36
+ is_active_item,
37
+ is_paused,
38
+ itemImage,
39
+ }) => {
40
+ const { theme } = useTheme();
41
+ const [expanded, setExpanded] = useState(false);
42
+
43
+ // Per-item countdown for live auction
44
+ const [secondsLeft, setSecondsLeft] = useState<number | null>(null);
45
+ const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
46
+
47
+ useEffect(() => {
48
+ if (countdownRef.current) clearInterval(countdownRef.current);
49
+ setSecondsLeft(null);
50
+
51
+ if (!item.item_deadline || item.status !== 'active' || is_paused) return;
52
+
53
+ const updateTimer = () => {
54
+ const deadline = new Date(item.item_deadline).getTime();
55
+ const diff = Math.max(0, (deadline - Date.now()) / 1000);
56
+ setSecondsLeft(diff);
57
+ if (diff <= 0 && countdownRef.current) {
58
+ clearInterval(countdownRef.current);
59
+ }
60
+ };
61
+
62
+ updateTimer();
63
+ countdownRef.current = setInterval(updateTimer, 100);
64
+ return () => {
65
+ if (countdownRef.current) clearInterval(countdownRef.current);
66
+ };
67
+ }, [item.item_deadline, item.status, is_paused]);
68
+
69
+ const canBid = (item.status === 'active' || item.status === 'pending') && !is_paused;
70
+ const statusColor = getItemStatusColor(item.status);
71
+ const isSold = item.status === 'sold';
72
+ const isUpcoming = auction_type === 'live' && item.status === 'pending';
73
+ const itemOpacity = (isSold || item.status === 'unsold') ? 0.5 : isUpcoming ? 0.7 : 1;
74
+
75
+ // Format seconds to display
76
+ const formatCountdown = (s: number) => {
77
+ if (s >= 60) {
78
+ const min = Math.floor(s / 60);
79
+ const sec = Math.floor(s % 60);
80
+ return `${min}:${sec.toString().padStart(2, '0')}`;
81
+ }
82
+ return s.toFixed(1);
83
+ };
84
+
85
+ return (
86
+ <View
87
+ variant="transparent"
88
+ style={[
89
+ styles.container,
90
+ { borderColor: theme.colors.border.subtle, opacity: itemOpacity },
91
+ is_active_item && { borderLeftWidth: 3, borderLeftColor: theme.colors.primary.default },
92
+ ]}
93
+ >
94
+ <TouchableOpacity
95
+ style={styles.row}
96
+ activeOpacity={0.7}
97
+ onPress={() => canBid && setExpanded(!expanded)}
98
+ disabled={!canBid}
99
+ >
100
+ {/* Item image / fallback */}
101
+ {(itemImage?.url || item.item_image?.url) ? (
102
+ <Image
103
+ source={{ uri: itemImage?.url || item.item_image?.url }}
104
+ style={[styles.imageContainer, { backgroundColor: theme.colors.surface.base }]}
105
+ resizeMode="contain"
106
+ />
107
+ ) : (
108
+ <View variant="transparent" style={[styles.imageContainer, { backgroundColor: theme.colors.surface.base }]}>
109
+ <Ionicons
110
+ name={item.item_type === 'team' ? 'people-outline' : 'person-outline'}
111
+ size={16}
112
+ color={theme.colors.text.tertiary}
113
+ />
114
+ </View>
115
+ )}
116
+
117
+ <View variant="transparent" style={styles.info}>
118
+ <View variant="transparent" style={styles.nameRow}>
119
+ <Text variant="body" bold numberOfLines={1} style={styles.name}>
120
+ {item.item_name}
121
+ </Text>
122
+ {item.seed != null && (
123
+ <Text variant="caption" color="tertiary" style={styles.seed}>
124
+ #{item.seed}
125
+ </Text>
126
+ )}
127
+ </View>
128
+ <View variant="transparent" style={styles.statusRow}>
129
+ <View variant="transparent" style={[styles.statusBadge, { backgroundColor: statusColor + '20' }]}>
130
+ <Text variant="caption" style={{ color: statusColor, fontSize: 10 }}>
131
+ {getStatusLabel(item.status)}
132
+ </Text>
133
+ </View>
134
+ {my_bid && (
135
+ <View variant="transparent" style={styles.bidStatus}>
136
+ <Ionicons
137
+ name={my_bid.bid_status === 'won' ? 'checkmark-circle' : 'ellipse'}
138
+ size={10}
139
+ color={getBidStatusColor(my_bid.bid_status)}
140
+ />
141
+ <Text variant="caption" style={{ color: getBidStatusColor(my_bid.bid_status), marginLeft: 4, fontSize: 10 }}>
142
+ {getStatusLabel(my_bid.bid_status)}
143
+ </Text>
144
+ </View>
145
+ )}
146
+ {/* Winner info for sold items */}
147
+ {isSold && item.winning_bid > 0 && (
148
+ <Text variant="caption" color="tertiary" style={{ marginLeft: 8, fontSize: 10 }}>
149
+ Sold: {formatCurrency(item.winning_bid)}
150
+ </Text>
151
+ )}
152
+ </View>
153
+ </View>
154
+
155
+ {/* Per-item countdown timer badge (live active item) */}
156
+ {is_active_item && secondsLeft != null && secondsLeft > 0 && !is_paused && (
157
+ <View
158
+ variant="transparent"
159
+ style={[
160
+ styles.timerBadge,
161
+ { backgroundColor: getTimerColor(secondsLeft, theme) + '20' },
162
+ secondsLeft < 5 && { opacity: Math.abs(Math.sin(Date.now() / 300)) * 0.5 + 0.5 },
163
+ ]}
164
+ >
165
+ <Ionicons name="timer-outline" size={12} color={getTimerColor(secondsLeft, theme)} />
166
+ <Text
167
+ variant="caption"
168
+ bold
169
+ style={{ color: getTimerColor(secondsLeft, theme), marginLeft: 3, fontSize: 11 }}
170
+ >
171
+ {formatCountdown(secondsLeft)}
172
+ </Text>
173
+ </View>
174
+ )}
175
+
176
+ {/* Going once indicator */}
177
+ {is_active_item && secondsLeft != null && secondsLeft <= 10 && secondsLeft > 0 && item.current_bid > 0 && !is_paused && (
178
+ <Text variant="caption" bold style={{ color: theme.colors.status.error, fontSize: 10, marginLeft: 4 }}>
179
+ Going...
180
+ </Text>
181
+ )}
182
+
183
+ <View variant="transparent" style={styles.bidInfo}>
184
+ <Text variant="caption" color="tertiary">
185
+ {auction_type === 'sealed_bid' ? 'Your Bid' : 'Current Bid'}
186
+ </Text>
187
+ <Text variant="body" bold>
188
+ {auction_type === 'sealed_bid'
189
+ ? my_bid
190
+ ? formatCurrency(my_bid.bid_amount)
191
+ : '-'
192
+ : item.current_bid > 0
193
+ ? formatCurrency(item.current_bid)
194
+ : '-'}
195
+ </Text>
196
+ </View>
197
+
198
+ {canBid && (
199
+ <Ionicons
200
+ name={expanded ? 'chevron-up' : 'chevron-down'}
201
+ size={16}
202
+ color={theme.colors.text.tertiary}
203
+ style={styles.chevron}
204
+ />
205
+ )}
206
+ </TouchableOpacity>
207
+
208
+ {expanded && canBid && (
209
+ <View variant="transparent" style={[styles.bidInputContainer, { borderColor: theme.colors.border.subtle }]}>
210
+ <CalcuttaBidInput
211
+ current_bid={item.current_bid}
212
+ min_bid={min_bid}
213
+ bid_increment={bid_increment}
214
+ auction_type={auction_type}
215
+ escrow_balance={escrow_balance}
216
+ existing_bid_amount={my_bid?.bid_amount}
217
+ onSubmit={onPlaceBid}
218
+ loading={false}
219
+ disabled={is_paused}
220
+ />
221
+ </View>
222
+ )}
223
+ </View>
224
+ );
225
+ };
226
+
227
+ const styles = StyleSheet.create({
228
+ container: {
229
+ borderBottomWidth: 1,
230
+ },
231
+ row: {
232
+ flexDirection: 'row',
233
+ alignItems: 'center',
234
+ padding: 12,
235
+ },
236
+ imageContainer: {
237
+ height: 32,
238
+ width: 32,
239
+ borderRadius: 16,
240
+ alignItems: 'center',
241
+ justifyContent: 'center',
242
+ },
243
+ info: {
244
+ flex: 1,
245
+ marginLeft: 10,
246
+ },
247
+ nameRow: {
248
+ flexDirection: 'row',
249
+ alignItems: 'center',
250
+ },
251
+ name: {
252
+ flex: 1,
253
+ },
254
+ seed: {
255
+ marginLeft: 6,
256
+ },
257
+ statusRow: {
258
+ flexDirection: 'row',
259
+ alignItems: 'center',
260
+ marginTop: 4,
261
+ },
262
+ statusBadge: {
263
+ paddingHorizontal: 6,
264
+ paddingVertical: 2,
265
+ borderRadius: 4,
266
+ },
267
+ bidStatus: {
268
+ flexDirection: 'row',
269
+ alignItems: 'center',
270
+ marginLeft: 8,
271
+ },
272
+ timerBadge: {
273
+ flexDirection: 'row',
274
+ alignItems: 'center',
275
+ paddingHorizontal: 6,
276
+ paddingVertical: 3,
277
+ borderRadius: 6,
278
+ marginLeft: 6,
279
+ },
280
+ bidInfo: {
281
+ alignItems: 'flex-end',
282
+ marginLeft: 10,
283
+ },
284
+ chevron: {
285
+ marginLeft: 8,
286
+ },
287
+ bidInputContainer: {
288
+ paddingHorizontal: 12,
289
+ paddingBottom: 12,
290
+ borderTopWidth: 1,
291
+ },
292
+ });
@@ -0,0 +1,214 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { StyleSheet, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import { formatCurrency } from '../helpers/formatting';
6
+
7
+ export interface CalcuttaBidInputProps {
8
+ current_bid: number;
9
+ min_bid: number;
10
+ bid_increment: number;
11
+ auction_type: 'sealed_bid' | 'live';
12
+ escrow_balance: number;
13
+ existing_bid_amount?: number;
14
+ onSubmit: (amount: number) => void;
15
+ loading: boolean;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export const CalcuttaBidInput: React.FC<CalcuttaBidInputProps> = ({
20
+ current_bid,
21
+ min_bid,
22
+ bid_increment,
23
+ auction_type,
24
+ escrow_balance,
25
+ existing_bid_amount,
26
+ onSubmit,
27
+ loading,
28
+ disabled,
29
+ }) => {
30
+ const { theme } = useTheme();
31
+
32
+ const minimumAllowed = auction_type === 'live'
33
+ ? Math.max(Number(min_bid), Number(current_bid) + Number(bid_increment))
34
+ : Number(min_bid);
35
+
36
+ const [amount, setAmount] = useState<string>(
37
+ existing_bid_amount ? existing_bid_amount.toString() : minimumAllowed.toString()
38
+ );
39
+ const [error, setError] = useState<string>('');
40
+
41
+ useEffect(() => {
42
+ if (!existing_bid_amount) {
43
+ setAmount(minimumAllowed.toString());
44
+ }
45
+ }, [minimumAllowed, existing_bid_amount]);
46
+
47
+ const numericAmount = parseFloat(amount) || 0;
48
+
49
+ const validate = (): boolean => {
50
+ if (isNaN(parseFloat(amount)) || numericAmount <= 0) {
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) {
64
+ setError(`Insufficient escrow. Available: ${formatCurrency(available)}`);
65
+ return false;
66
+ }
67
+ setError('');
68
+ return true;
69
+ };
70
+
71
+ const handleSubmit = () => {
72
+ if (validate()) {
73
+ onSubmit(numericAmount);
74
+ }
75
+ };
76
+
77
+ const adjustAmount = (direction: 'up' | 'down') => {
78
+ const step = Number(bid_increment) > 0 ? Number(bid_increment) : 1;
79
+ const current = numericAmount;
80
+ const newAmount = direction === 'up' ? current + step : Math.max(minimumAllowed, current - step);
81
+ setAmount(newAmount.toString());
82
+ setError('');
83
+ };
84
+
85
+ return (
86
+ <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
+ {disabled ? (
146
+ <View variant="transparent" style={[styles.submitButton, { backgroundColor: theme.colors.surface.base }]}>
147
+ <Text variant="body" bold style={{ color: theme.colors.text.tertiary }}>
148
+ Paused
149
+ </Text>
150
+ </View>
151
+ ) : (
152
+ <TouchableOpacity
153
+ style={[styles.submitButton, { backgroundColor: theme.colors.primary.default }]}
154
+ onPress={handleSubmit}
155
+ disabled={loading}
156
+ activeOpacity={0.7}
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'}
163
+ </Text>
164
+ )}
165
+ </TouchableOpacity>
166
+ )}
167
+ </View>
168
+ );
169
+ };
170
+
171
+ const styles = StyleSheet.create({
172
+ container: {
173
+ paddingTop: 10,
174
+ },
175
+ inputRow: {
176
+ flexDirection: 'row',
177
+ alignItems: 'center',
178
+ },
179
+ adjustButton: {
180
+ width: 36,
181
+ height: 36,
182
+ borderRadius: 8,
183
+ borderWidth: 1,
184
+ alignItems: 'center',
185
+ justifyContent: 'center',
186
+ },
187
+ input: {
188
+ flex: 1,
189
+ height: 36,
190
+ borderRadius: 8,
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,
203
+ },
204
+ submitButton: {
205
+ marginTop: 10,
206
+ paddingVertical: 10,
207
+ borderRadius: 8,
208
+ alignItems: 'center',
209
+ justifyContent: 'center',
210
+ },
211
+ submitText: {
212
+ color: '#FFFFFF',
213
+ },
214
+ });
@@ -0,0 +1,131 @@
1
+ import React 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 { CalcuttaCompetitionProps, CalcuttaParticipantProps } from '@bettoredge/types';
6
+ import { formatCurrency, getStatusLabel } from '../helpers/formatting';
7
+
8
+ export interface CalcuttaCardProps {
9
+ competition: CalcuttaCompetitionProps;
10
+ participant?: CalcuttaParticipantProps;
11
+ onSelect: (competition: CalcuttaCompetitionProps) => void;
12
+ }
13
+
14
+ export const CalcuttaCard: React.FC<CalcuttaCardProps> = ({
15
+ competition,
16
+ participant,
17
+ onSelect,
18
+ }) => {
19
+ const { theme } = useTheme();
20
+
21
+ const statusLabel = getStatusLabel(competition.status);
22
+ const auctionLabel = competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid';
23
+ const participantCount = competition.participants?.length ?? 0;
24
+
25
+ return (
26
+ <TouchableOpacity
27
+ style={[styles.row, { borderColor: theme.colors.border.subtle }]}
28
+ activeOpacity={0.7}
29
+ onPress={() => onSelect(competition)}
30
+ >
31
+ {competition.image?.url ? (
32
+ <Image
33
+ source={{ uri: competition.image.url }}
34
+ style={[styles.image, { backgroundColor: theme.colors.surface.base }]}
35
+ resizeMode="cover"
36
+ />
37
+ ) : (
38
+ <View
39
+ variant="transparent"
40
+ style={[styles.image, styles.imageFallback, { backgroundColor: theme.colors.surface.base }]}
41
+ >
42
+ <Ionicons name="trophy-outline" size={18} color={theme.colors.text.tertiary} />
43
+ </View>
44
+ )}
45
+
46
+ <View variant="transparent" style={styles.info}>
47
+ <Text variant="body" bold numberOfLines={1}>
48
+ {competition.competition_name}
49
+ </Text>
50
+ <View variant="transparent" style={styles.meta}>
51
+ <Text variant="caption" color="tertiary">
52
+ {statusLabel}
53
+ </Text>
54
+ <Text variant="caption" color="tertiary"> {'\u00B7'} </Text>
55
+ <Text variant="caption" color="tertiary">
56
+ {auctionLabel}
57
+ </Text>
58
+ </View>
59
+ <View variant="transparent" style={styles.meta}>
60
+ <Text variant="caption" color="secondary">
61
+ Pot: {formatCurrency(competition.total_pot, competition.market_type)}
62
+ </Text>
63
+ <Text variant="caption" color="tertiary"> {'\u00B7'} </Text>
64
+ <Text variant="caption" color="secondary">
65
+ {participantCount} participant{participantCount !== 1 ? 's' : ''}
66
+ </Text>
67
+ </View>
68
+ </View>
69
+
70
+ {participant && (
71
+ <View variant="transparent" style={styles.participantInfo}>
72
+ <Text variant="caption" color="secondary">
73
+ {participant.items_owned} item{participant.items_owned !== 1 ? 's' : ''}
74
+ </Text>
75
+ <Text
76
+ variant="caption"
77
+ style={{
78
+ color: participant.total_winnings > 0
79
+ ? theme.colors.status.success
80
+ : theme.colors.text.secondary,
81
+ marginTop: 2,
82
+ }}
83
+ >
84
+ {formatCurrency(participant.total_winnings, competition.market_type)}
85
+ </Text>
86
+ </View>
87
+ )}
88
+
89
+ <Ionicons
90
+ name="chevron-forward"
91
+ size={16}
92
+ color={theme.colors.text.tertiary}
93
+ style={styles.chevron}
94
+ />
95
+ </TouchableOpacity>
96
+ );
97
+ };
98
+
99
+ const styles = StyleSheet.create({
100
+ row: {
101
+ flexDirection: 'row',
102
+ alignItems: 'center',
103
+ padding: 12,
104
+ borderBottomWidth: 1,
105
+ },
106
+ image: {
107
+ height: 36,
108
+ width: 36,
109
+ borderRadius: 8,
110
+ },
111
+ imageFallback: {
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ },
115
+ info: {
116
+ flex: 1,
117
+ marginLeft: 10,
118
+ },
119
+ meta: {
120
+ flexDirection: 'row',
121
+ alignItems: 'center',
122
+ marginTop: 2,
123
+ },
124
+ participantInfo: {
125
+ marginHorizontal: 10,
126
+ alignItems: 'flex-end',
127
+ },
128
+ chevron: {
129
+ marginLeft: 4,
130
+ },
131
+ });