@bettoredge/calcutta 0.3.0 → 0.4.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.
@@ -66,10 +66,25 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
66
66
  const hasBalance = player_balance != null;
67
67
  const insufficientBalance = hasBalance && action === 'transfer_in' && numericAmount > Number(player_balance);
68
68
  const shortfall = insufficientBalance ? numericAmount - Number(player_balance) : 0;
69
+ const exceedsBudget = action === 'transfer_in' && budgetRemaining !== null && numericAmount > budgetRemaining;
70
+ const exceedsWithdraw = action === 'transfer_out' && numericAmount > available;
71
+ const [actionError, setActionError] = useState('');
72
+
73
+ const isInvalidAmount = numericAmount <= 0 || exceedsBudget || exceedsWithdraw || (insufficientBalance && !onDepositFunds);
74
+
75
+ const getValidationMessage = (): string => {
76
+ if (numericAmount <= 0) return '';
77
+ if (exceedsBudget) return `Budget cap: ${formatCurrency(Number(max_escrow), market_type)}. You can deposit up to ${formatCurrency(budgetRemaining ?? 0, market_type)} more.`;
78
+ if (exceedsWithdraw) return `You only have ${formatCurrency(available, market_type)} available to withdraw.`;
79
+ if (insufficientBalance && !onDepositFunds) return `Insufficient wallet balance. You have ${formatCurrency(player_balance, market_type)}.`;
80
+ return '';
81
+ };
82
+
83
+ const validationMessage = getValidationMessage();
69
84
 
70
85
  const handleAction = async () => {
71
86
  if (isNaN(numericAmount) || numericAmount <= 0) return;
72
- if (action === 'transfer_in' && budgetRemaining !== null && numericAmount > budgetRemaining) return;
87
+ setActionError('');
73
88
  if (insufficientBalance && onDepositFunds) {
74
89
  onDepositFunds(Math.ceil(shortfall * 100) / 100);
75
90
  return;
@@ -84,12 +99,22 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
84
99
  if (result) {
85
100
  onUpdate(result);
86
101
  setAmount('');
102
+ setActionError('');
87
103
  }
88
- } catch {}
104
+ } catch (e: any) {
105
+ setActionError(e?.message || 'Transfer failed. Please try again.');
106
+ }
89
107
  };
90
108
 
91
109
  const handleQuickAmount = (amt: number) => {
92
- setAmount(String(amt));
110
+ // Cap quick amount to budget remaining for deposits
111
+ if (action === 'transfer_in' && budgetRemaining !== null) {
112
+ setAmount(String(Math.min(amt, budgetRemaining)));
113
+ } else if (action === 'transfer_out') {
114
+ setAmount(String(Math.min(amt, available)));
115
+ } else {
116
+ setAmount(String(amt));
117
+ }
93
118
  };
94
119
 
95
120
  // ============================================
@@ -278,14 +303,14 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
278
303
  </View>
279
304
  <TouchableOpacity
280
305
  style={[styles.submitBtn, {
281
- backgroundColor: insufficientBalance
306
+ backgroundColor: insufficientBalance && onDepositFunds
282
307
  ? theme.colors.status.error
283
- : (loading || !amount || numericAmount <= 0 || (action === 'transfer_in' && budgetFullyDeposited))
308
+ : (loading || isInvalidAmount)
284
309
  ? theme.colors.surface.elevated
285
310
  : theme.colors.primary.default,
286
311
  }]}
287
312
  onPress={handleAction}
288
- disabled={loading || !amount || numericAmount <= 0 || (action === 'transfer_in' && budgetFullyDeposited)}
313
+ disabled={loading || (isInvalidAmount && !(insufficientBalance && onDepositFunds))}
289
314
  activeOpacity={0.7}
290
315
  >
291
316
  {loading ? (
@@ -295,13 +320,13 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
295
320
  <Ionicons
296
321
  name={action === 'transfer_in' ? 'arrow-down-circle' : 'arrow-up-circle'}
297
322
  size={20}
298
- color={(!amount || numericAmount <= 0) ? theme.colors.text.tertiary : '#FFFFFF'}
323
+ color={isInvalidAmount ? theme.colors.text.tertiary : '#FFFFFF'}
299
324
  />
300
325
  <Text
301
326
  variant="body"
302
327
  bold
303
328
  style={{
304
- color: (!amount || numericAmount <= 0) ? theme.colors.text.tertiary : '#FFFFFF',
329
+ color: isInvalidAmount ? theme.colors.text.tertiary : '#FFFFFF',
305
330
  marginLeft: 6,
306
331
  }}
307
332
  >
@@ -316,8 +341,18 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
316
341
  </TouchableOpacity>
317
342
  </View>
318
343
 
319
- {/* Insufficient balance */}
320
- {insufficientBalance && (
344
+ {/* Validation message (budget cap, withdraw limit, etc.) */}
345
+ {validationMessage ? (
346
+ <View variant="transparent" style={[styles.warningRow, { backgroundColor: '#D9770615' }]}>
347
+ <Ionicons name="information-circle-outline" size={16} color="#D97706" />
348
+ <Text variant="caption" style={{ color: '#D97706', marginLeft: 8, flex: 1 }}>
349
+ {validationMessage}
350
+ </Text>
351
+ </View>
352
+ ) : null}
353
+
354
+ {/* Insufficient wallet balance */}
355
+ {insufficientBalance && !validationMessage && (
321
356
  <View variant="transparent" style={[styles.warningRow, { backgroundColor: theme.colors.status.error + '10' }]}>
322
357
  <Ionicons name="warning-outline" size={16} color={theme.colors.status.error} />
323
358
  <Text variant="caption" style={{ color: theme.colors.status.error, marginLeft: 8, flex: 1 }}>
@@ -326,12 +361,12 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
326
361
  </View>
327
362
  )}
328
363
 
329
- {/* Error */}
330
- {error && (
364
+ {/* API error */}
365
+ {(error || actionError) && (
331
366
  <View variant="transparent" style={[styles.warningRow, { backgroundColor: theme.colors.status.error + '10' }]}>
332
367
  <Ionicons name="alert-circle-outline" size={16} color={theme.colors.status.error} />
333
368
  <Text variant="caption" style={{ color: theme.colors.status.error, marginLeft: 8, flex: 1 }}>
334
- {error}
369
+ {actionError || error}
335
370
  </Text>
336
371
  </View>
337
372
  )}
@@ -442,6 +477,7 @@ const styles = StyleSheet.create({
442
477
  input: {
443
478
  flex: 1,
444
479
  fontSize: 18,
480
+ lineHeight: 24,
445
481
  height: 48,
446
482
  padding: 0,
447
483
  },
@@ -74,7 +74,7 @@ export const CalcuttaLeaderboard: React.FC<CalcuttaLeaderboardProps> = ({
74
74
  <Image source={{ uri: profilePic }} style={styles.avatar} />
75
75
  ) : (
76
76
  <View variant="transparent" style={[styles.avatar, { backgroundColor: theme.colors.primary.subtle }]}>
77
- <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10 }}>
77
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10, lineHeight: 13 }}>
78
78
  {username.charAt(0).toUpperCase()}
79
79
  </Text>
80
80
  </View>
@@ -84,7 +84,7 @@ export const CalcuttaLeaderboard: React.FC<CalcuttaLeaderboardProps> = ({
84
84
  <Text variant="body" bold={isMe} numberOfLines={1} style={{ flex: 1 }}>{username}</Text>
85
85
  {isMe && (
86
86
  <View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
87
- <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 9 }}>YOU</Text>
87
+ <Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 9, lineHeight: 12 }}>YOU</Text>
88
88
  </View>
89
89
  )}
90
90
  </View>
@@ -0,0 +1,176 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { StyleSheet, TouchableOpacity, LayoutAnimation, Platform, UIManager } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaEscrowProps } from '@bettoredge/types';
6
+ import { useCalcuttaEscrow } from '../hooks/useCalcuttaEscrow';
7
+ import { formatCurrency } from '../helpers/formatting';
8
+ import { CalcuttaEscrow } from './CalcuttaEscrow';
9
+
10
+ if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
11
+ UIManager.setLayoutAnimationEnabledExperimental(true);
12
+ }
13
+
14
+ export interface EscrowWidgetProps {
15
+ calcutta_competition_id: string;
16
+ market_type: string;
17
+ max_escrow?: number;
18
+ player_balance?: number;
19
+ onDepositFunds?: (amount: number) => void;
20
+ readOnly?: boolean;
21
+ onUpdate?: () => void;
22
+ initialExpanded?: boolean;
23
+ }
24
+
25
+ export const EscrowWidget: React.FC<EscrowWidgetProps> = ({
26
+ calcutta_competition_id,
27
+ market_type,
28
+ max_escrow,
29
+ player_balance,
30
+ onDepositFunds,
31
+ readOnly,
32
+ onUpdate,
33
+ initialExpanded,
34
+ }) => {
35
+ const { theme } = useTheme();
36
+ const { escrow, fetchEscrow } = useCalcuttaEscrow(calcutta_competition_id);
37
+ const [expanded, setExpanded] = useState(initialExpanded ?? false);
38
+
39
+ useEffect(() => {
40
+ if (initialExpanded) setExpanded(true);
41
+ }, [initialExpanded]);
42
+
43
+ useEffect(() => {
44
+ fetchEscrow();
45
+ }, []);
46
+
47
+ const available = Number(escrow?.escrow_balance ?? 0);
48
+ const committed = Number(escrow?.committed_balance ?? 0);
49
+ const hasFunds = available > 0 || committed > 0;
50
+
51
+ const toggle = () => {
52
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
53
+ setExpanded(!expanded);
54
+ };
55
+
56
+ return (
57
+ <View variant="transparent" style={[styles.container, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
58
+ {/* Collapsed header — always visible */}
59
+ <TouchableOpacity
60
+ style={styles.header}
61
+ onPress={readOnly ? undefined : toggle}
62
+ activeOpacity={readOnly ? 1 : 0.7}
63
+ >
64
+ <View variant="transparent" style={[styles.iconCircle, { backgroundColor: hasFunds ? theme.colors.status.successSubtle : theme.colors.primary.subtle }]}>
65
+ <Ionicons
66
+ name="wallet-outline"
67
+ size={18}
68
+ color={hasFunds ? theme.colors.status.success : theme.colors.primary.default}
69
+ />
70
+ </View>
71
+ <View variant="transparent" style={styles.headerText}>
72
+ <Text variant="body" bold>
73
+ {formatCurrency(available, market_type)}
74
+ <Text variant="caption" color="tertiary"> available</Text>
75
+ </Text>
76
+ {committed > 0 && (
77
+ <Text variant="caption" color="tertiary">
78
+ {formatCurrency(committed, market_type)} committed
79
+ </Text>
80
+ )}
81
+ </View>
82
+ {!readOnly && (
83
+ <View variant="transparent" style={[styles.addBtn, { backgroundColor: theme.colors.primary.default }]}>
84
+ <Ionicons name={expanded ? 'chevron-up' : 'add'} size={18} color="#FFFFFF" />
85
+ {!expanded && (
86
+ <Text variant="caption" bold style={{ color: '#FFFFFF', marginLeft: 4 }}>
87
+ {hasFunds ? 'Manage' : 'Add Funds'}
88
+ </Text>
89
+ )}
90
+ </View>
91
+ )}
92
+ </TouchableOpacity>
93
+
94
+ {/* Budget cap bar (always visible if applicable) */}
95
+ {max_escrow != null && Number(max_escrow) > 0 && (
96
+ <View variant="transparent" style={styles.budgetBar}>
97
+ <View variant="transparent" style={[styles.barTrack, { backgroundColor: theme.colors.surface.base }]}>
98
+ <View variant="transparent" style={[styles.barFill, {
99
+ width: `${Math.min(100, ((Number(escrow?.total_deposited ?? 0) - Number(escrow?.total_withdrawn ?? 0)) / Number(max_escrow)) * 100)}%`,
100
+ backgroundColor: theme.colors.primary.default,
101
+ }]} />
102
+ </View>
103
+ <Text variant="caption" color="tertiary" style={{ marginTop: 4 }}>
104
+ Budget: {formatCurrency(Number(max_escrow), market_type)}
105
+ </Text>
106
+ </View>
107
+ )}
108
+
109
+ {/* Expanded: full escrow management */}
110
+ {expanded && !readOnly && (
111
+ <View variant="transparent" style={styles.expandedContent}>
112
+ <CalcuttaEscrow
113
+ calcutta_competition_id={calcutta_competition_id}
114
+ escrow={escrow ?? undefined}
115
+ market_type={market_type}
116
+ max_escrow={max_escrow}
117
+ player_balance={player_balance}
118
+ onDepositFunds={onDepositFunds}
119
+ onUpdate={(updated) => {
120
+ fetchEscrow();
121
+ onUpdate?.();
122
+ }}
123
+ />
124
+ </View>
125
+ )}
126
+ </View>
127
+ );
128
+ };
129
+
130
+ const styles = StyleSheet.create({
131
+ container: {
132
+ borderRadius: 14,
133
+ borderWidth: 1,
134
+ overflow: 'hidden',
135
+ },
136
+ header: {
137
+ flexDirection: 'row',
138
+ alignItems: 'center',
139
+ padding: 14,
140
+ },
141
+ iconCircle: {
142
+ width: 36,
143
+ height: 36,
144
+ borderRadius: 18,
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ },
148
+ headerText: {
149
+ flex: 1,
150
+ marginLeft: 12,
151
+ },
152
+ addBtn: {
153
+ flexDirection: 'row',
154
+ alignItems: 'center',
155
+ paddingHorizontal: 12,
156
+ paddingVertical: 6,
157
+ borderRadius: 20,
158
+ },
159
+ budgetBar: {
160
+ paddingHorizontal: 14,
161
+ paddingBottom: 12,
162
+ },
163
+ barTrack: {
164
+ height: 4,
165
+ borderRadius: 2,
166
+ overflow: 'hidden',
167
+ },
168
+ barFill: {
169
+ height: 4,
170
+ borderRadius: 2,
171
+ },
172
+ expandedContent: {
173
+ borderTopWidth: 1,
174
+ borderTopColor: 'rgba(128,128,128,0.15)',
175
+ },
176
+ });
@@ -0,0 +1,288 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { StyleSheet, Animated, Image } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaAuctionItemProps } from '@bettoredge/types';
6
+ import { formatCurrency } from '../helpers/formatting';
7
+ import { CalcuttaCountdown } from './CalcuttaCountdown';
8
+
9
+ export interface ItemSoldCelebrationProps {
10
+ visible: boolean;
11
+ item?: CalcuttaAuctionItemProps;
12
+ winnerName?: string;
13
+ winnerProfilePic?: string;
14
+ winningBid?: number;
15
+ isMe?: boolean;
16
+ marketType?: string;
17
+ /** Next item coming up (shown at bottom of modal) */
18
+ nextItem?: CalcuttaAuctionItemProps;
19
+ nextItemDeadline?: string;
20
+ /** Auto-dismiss after this many ms (default 5000) */
21
+ autoDismissMs?: number;
22
+ onDismiss: () => void;
23
+ itemImage?: string;
24
+ }
25
+
26
+ export const ItemSoldCelebration: React.FC<ItemSoldCelebrationProps> = ({
27
+ visible,
28
+ item,
29
+ winnerName,
30
+ winnerProfilePic,
31
+ winningBid,
32
+ isMe,
33
+ marketType = 'FOR_MONEY',
34
+ nextItem,
35
+ nextItemDeadline,
36
+ autoDismissMs = 5000,
37
+ onDismiss,
38
+ itemImage,
39
+ }) => {
40
+ const { theme } = useTheme();
41
+ const opacityAnim = useRef(new Animated.Value(0)).current;
42
+ const scaleAnim = useRef(new Animated.Value(0.8)).current;
43
+ const trophyBounce = useRef(new Animated.Value(0)).current;
44
+ const confettiAnim = useRef(new Animated.Value(0)).current;
45
+
46
+ useEffect(() => {
47
+ if (!visible) {
48
+ Animated.timing(opacityAnim, { toValue: 0, duration: 300, useNativeDriver: true }).start();
49
+ return;
50
+ }
51
+
52
+ // Entrance
53
+ opacityAnim.setValue(0);
54
+ scaleAnim.setValue(0.8);
55
+ trophyBounce.setValue(0);
56
+ confettiAnim.setValue(0);
57
+
58
+ Animated.parallel([
59
+ Animated.timing(opacityAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
60
+ Animated.spring(scaleAnim, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }),
61
+ ]).start(() => {
62
+ // Trophy bounce
63
+ Animated.sequence([
64
+ Animated.timing(trophyBounce, { toValue: -20, duration: 200, useNativeDriver: true }),
65
+ Animated.spring(trophyBounce, { toValue: 0, friction: 3, tension: 120, useNativeDriver: true }),
66
+ ]).start();
67
+
68
+ // Confetti burst
69
+ Animated.timing(confettiAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start();
70
+ });
71
+
72
+ // Auto-dismiss
73
+ const timer = setTimeout(() => {
74
+ Animated.timing(opacityAnim, { toValue: 0, duration: 400, useNativeDriver: true }).start(() => onDismiss());
75
+ }, autoDismissMs);
76
+
77
+ return () => clearTimeout(timer);
78
+ }, [visible]);
79
+
80
+ if (!visible || !item) return null;
81
+
82
+ const accentColor = isMe ? '#10B981' : '#F59E0B';
83
+
84
+ return (
85
+ <Animated.View style={[styles.overlay, { opacity: opacityAnim }]}>
86
+ <View variant="transparent" style={styles.backdrop} />
87
+ <Animated.View style={[styles.card, { transform: [{ scale: scaleAnim }] }]}>
88
+ {/* Confetti dots */}
89
+ {[...Array(12)].map((_, i) => {
90
+ const angle = (i / 12) * Math.PI * 2;
91
+ const colors = ['#F59E0B', '#10B981', '#3B82F6', '#EF4444', '#8B5CF6', '#EC4899'];
92
+ return (
93
+ <Animated.View
94
+ key={i}
95
+ style={{
96
+ ...StyleSheet.absoluteFillObject,
97
+ top: '50%',
98
+ left: '50%',
99
+ width: 8,
100
+ height: 8,
101
+ borderRadius: 4,
102
+ backgroundColor: colors[i % colors.length],
103
+ opacity: confettiAnim,
104
+ transform: [
105
+ { translateX: Animated.multiply(confettiAnim, Math.cos(angle) * 120) as any },
106
+ { translateY: Animated.multiply(confettiAnim, Math.sin(angle) * 120 - 40) as any },
107
+ { scale: Animated.subtract(1, Animated.multiply(confettiAnim, 0.5)) as any },
108
+ ],
109
+ }}
110
+ />
111
+ );
112
+ })}
113
+
114
+ {/* Trophy */}
115
+ <Animated.View style={{ transform: [{ translateY: trophyBounce }], marginBottom: 16 }}>
116
+ <View variant="transparent" style={[styles.trophyCircle, { backgroundColor: accentColor + '20' }]}>
117
+ <Ionicons name="trophy" size={40} color={accentColor} />
118
+ </View>
119
+ </Animated.View>
120
+
121
+ {/* SOLD label */}
122
+ <Text style={[styles.soldLabel, { color: accentColor }]}>SOLD!</Text>
123
+
124
+ {/* Item info */}
125
+ <View variant="transparent" style={styles.itemRow}>
126
+ {itemImage ? (
127
+ <Image source={{ uri: itemImage }} style={styles.itemImage} resizeMode="cover" />
128
+ ) : (
129
+ <View variant="transparent" style={[styles.itemImage, { backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }]}>
130
+ <Ionicons name="trophy-outline" size={20} color={theme.colors.text.tertiary} />
131
+ </View>
132
+ )}
133
+ <View variant="transparent" style={{ flex: 1, marginLeft: 12 }}>
134
+ <Text style={styles.itemName}>{item.item_name}</Text>
135
+ {item.seed != null && <Text style={styles.itemSeed}>Seed #{item.seed}</Text>}
136
+ </View>
137
+ </View>
138
+
139
+ {/* Winner + bid */}
140
+ <View variant="transparent" style={[styles.winnerRow, { backgroundColor: accentColor + '10', borderColor: accentColor + '30' }]}>
141
+ {winnerProfilePic ? (
142
+ <Image source={{ uri: winnerProfilePic }} style={styles.winnerAvatar} />
143
+ ) : (
144
+ <View variant="transparent" style={[styles.winnerAvatar, { backgroundColor: accentColor + '30', alignItems: 'center', justifyContent: 'center' }]}>
145
+ <Text style={{ color: accentColor, fontWeight: '700', fontSize: 14, lineHeight: 19 }}>
146
+ {(winnerName || '?').charAt(0).toUpperCase()}
147
+ </Text>
148
+ </View>
149
+ )}
150
+ <View variant="transparent" style={{ flex: 1, marginLeft: 10 }}>
151
+ <Text style={[styles.winnerName, { color: accentColor }]}>
152
+ {isMe ? 'You won!' : winnerName || 'Winner'}
153
+ </Text>
154
+ <Text style={styles.bidAmount}>
155
+ {formatCurrency(winningBid ?? 0, marketType)}
156
+ </Text>
157
+ </View>
158
+ {isMe && <Ionicons name="checkmark-circle" size={28} color={accentColor} />}
159
+ </View>
160
+
161
+ {/* Next item teaser */}
162
+ {nextItem && (
163
+ <View variant="transparent" style={styles.nextSection}>
164
+ <View variant="transparent" style={styles.divider} />
165
+ <Text style={styles.nextLabel}>Next up</Text>
166
+ <Text style={styles.nextItemName}>{nextItem.item_name}</Text>
167
+ {nextItemDeadline && (
168
+ <View variant="transparent" style={{ marginTop: 6 }}>
169
+ <CalcuttaCountdown targetDate={nextItemDeadline} label="Bidding starts" size="compact" />
170
+ </View>
171
+ )}
172
+ </View>
173
+ )}
174
+ </Animated.View>
175
+ </Animated.View>
176
+ );
177
+ };
178
+
179
+ const styles = StyleSheet.create({
180
+ overlay: {
181
+ ...StyleSheet.absoluteFillObject,
182
+ zIndex: 998,
183
+ alignItems: 'center',
184
+ justifyContent: 'center',
185
+ },
186
+ backdrop: {
187
+ ...StyleSheet.absoluteFillObject,
188
+ backgroundColor: 'rgba(0,0,0,0.75)',
189
+ },
190
+ card: {
191
+ backgroundColor: '#1a1a2e',
192
+ borderRadius: 24,
193
+ padding: 28,
194
+ alignItems: 'center',
195
+ width: '85%',
196
+ maxWidth: 360,
197
+ overflow: 'visible',
198
+ },
199
+ trophyCircle: {
200
+ width: 80,
201
+ height: 80,
202
+ borderRadius: 40,
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ },
206
+ soldLabel: {
207
+ fontSize: 28,
208
+ lineHeight: 37,
209
+ fontWeight: '900',
210
+ letterSpacing: 4,
211
+ marginBottom: 20,
212
+ },
213
+ itemRow: {
214
+ flexDirection: 'row',
215
+ alignItems: 'center',
216
+ width: '100%',
217
+ marginBottom: 16,
218
+ },
219
+ itemImage: {
220
+ width: 48,
221
+ height: 48,
222
+ borderRadius: 10,
223
+ },
224
+ itemName: {
225
+ color: '#FFFFFF',
226
+ fontSize: 18,
227
+ lineHeight: 24,
228
+ fontWeight: '700',
229
+ },
230
+ itemSeed: {
231
+ color: 'rgba(255,255,255,0.5)',
232
+ fontSize: 13,
233
+ lineHeight: 17,
234
+ marginTop: 2,
235
+ },
236
+ winnerRow: {
237
+ flexDirection: 'row',
238
+ alignItems: 'center',
239
+ width: '100%',
240
+ padding: 14,
241
+ borderRadius: 14,
242
+ borderWidth: 1,
243
+ },
244
+ winnerAvatar: {
245
+ width: 36,
246
+ height: 36,
247
+ borderRadius: 18,
248
+ overflow: 'hidden',
249
+ },
250
+ winnerName: {
251
+ fontSize: 15,
252
+ lineHeight: 20,
253
+ fontWeight: '700',
254
+ },
255
+ bidAmount: {
256
+ color: '#FFFFFF',
257
+ fontSize: 18,
258
+ lineHeight: 24,
259
+ fontWeight: '800',
260
+ marginTop: 2,
261
+ },
262
+ nextSection: {
263
+ width: '100%',
264
+ alignItems: 'center',
265
+ marginTop: 16,
266
+ },
267
+ divider: {
268
+ width: 40,
269
+ height: 2,
270
+ backgroundColor: 'rgba(255,255,255,0.15)',
271
+ borderRadius: 1,
272
+ marginBottom: 12,
273
+ },
274
+ nextLabel: {
275
+ color: 'rgba(255,255,255,0.4)',
276
+ fontSize: 12,
277
+ lineHeight: 16,
278
+ textTransform: 'uppercase',
279
+ letterSpacing: 1,
280
+ },
281
+ nextItemName: {
282
+ color: '#FFFFFF',
283
+ fontSize: 16,
284
+ lineHeight: 21,
285
+ fontWeight: '600',
286
+ marginTop: 4,
287
+ },
288
+ });
@@ -146,8 +146,8 @@ const styles = StyleSheet.create({
146
146
  },
147
147
  emoji: {
148
148
  fontSize: 48,
149
+ lineHeight: 63,
149
150
  marginBottom: 8,
150
- lineHeight: 56,
151
151
  },
152
152
  itemSection: {
153
153
  padding: 24,
@@ -26,7 +26,9 @@ import { CalcuttaLeaderboard } from '../CalcuttaLeaderboard';
26
26
  interface SealedBidAuctionProps {
27
27
  calcutta_competition_id: string;
28
28
  player_id?: string;
29
- onClose: () => void;
29
+ onClose?: () => void;
30
+ access_token?: string;
31
+ device_id?: string;
30
32
  getSocket?: () => WebSocket | null;
31
33
  getAccessToken?: () => string | undefined;
32
34
  getDeviceId?: () => string | undefined;
@@ -53,9 +55,8 @@ export const SealedBidAuction: React.FC<SealedBidAuctionProps> = ({
53
55
  calcutta_competition_id,
54
56
  player_id,
55
57
  onClose,
56
- getSocket,
57
- getAccessToken,
58
- getDeviceId,
58
+ access_token,
59
+ device_id,
59
60
  player_data,
60
61
  player_balance,
61
62
  onDepositFunds,
@@ -145,14 +146,11 @@ export const SealedBidAuction: React.FC<SealedBidAuctionProps> = ({
145
146
  const { images: itemImages } = useCalcuttaItemImages(items);
146
147
 
147
148
  // Socket for presence
148
- const noopSocket = useCallback(() => null, []);
149
- const noopString = useCallback(() => undefined, []);
150
149
  const { connected: socketConnected, presence } = useCalcuttaSocket(
151
150
  calcutta_competition_id,
151
+ access_token,
152
+ device_id,
152
153
  player_data || { username: '' },
153
- getSocket || noopSocket,
154
- getAccessToken || noopString,
155
- getDeviceId || noopString,
156
154
  );
157
155
 
158
156
  // Adaptive polling based on lifecycle state