@bettoredge/calcutta 0.3.1 → 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.
- package/package.json +3 -2
- package/src/components/AuctionCompleteOverlay.tsx +306 -0
- package/src/components/AuctionCountdownOverlay.tsx +178 -0
- package/src/components/AuctionInfoChips.tsx +105 -0
- package/src/components/AuctionPausedOverlay.tsx +154 -0
- package/src/components/CalcuttaActionCard.tsx +291 -0
- package/src/components/CalcuttaAuction.tsx +677 -281
- package/src/components/CalcuttaAuctionItem.tsx +195 -106
- package/src/components/CalcuttaBidInput.tsx +130 -139
- package/src/components/CalcuttaCountdown.tsx +183 -0
- package/src/components/CalcuttaDetail.tsx +211 -144
- package/src/components/CalcuttaEscrow.tsx +49 -13
- package/src/components/CalcuttaLeaderboard.tsx +2 -2
- package/src/components/EscrowWidget.tsx +176 -0
- package/src/components/ItemSoldCelebration.tsx +288 -0
- package/src/components/SweepstakesReveal.tsx +1 -1
- package/src/components/sealed/SealedBidAuction.tsx +7 -9
- package/src/components/sealed/SealedBidHeader.tsx +3 -3
- package/src/components/sealed/SealedBidItemCard.tsx +9 -9
- package/src/components/sealed/SealedBidItemsTab.tsx +2 -1
- package/src/components/sealed/SealedBidMyBidsTab.tsx +3 -3
- package/src/components/sealed/SealedBidPlayersTab.tsx +2 -2
- package/src/components/sealed/SealedBidStatusBar.tsx +1 -1
- package/src/components/sealed/SealedBidTabBar.tsx +2 -0
- package/src/hooks/useCalcuttaAuction.ts +16 -2
- package/src/hooks/useCalcuttaEscrow.ts +5 -1
- package/src/hooks/useCalcuttaSocket.ts +80 -82
- package/src/index.ts +18 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useMemo, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
|
2
2
|
import { StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Image, FlatList, TextInput, Platform } from 'react-native';
|
|
3
3
|
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
4
|
import { Ionicons } from '@expo/vector-icons';
|
|
@@ -7,16 +7,30 @@ import { useCalcuttaCompetition } from '../hooks/useCalcuttaCompetition';
|
|
|
7
7
|
import { useCalcuttaPlayers } from '../hooks/useCalcuttaPlayers';
|
|
8
8
|
import { useCalcuttaItemImages } from '../hooks/useCalcuttaItemImages';
|
|
9
9
|
import { formatCurrency, getStatusLabel, resolveItemImageUrl } from '../helpers/formatting';
|
|
10
|
-
import {
|
|
10
|
+
import { deriveLifecycleState, canManageEscrow } from '../helpers/lifecycleState';
|
|
11
|
+
import { useCalcuttaEscrow } from '../hooks/useCalcuttaEscrow';
|
|
12
|
+
import { startSweepstakesCompetition, startCalcuttaAuction } from '@bettoredge/api';
|
|
13
|
+
import { CalcuttaActionCard } from './CalcuttaActionCard';
|
|
14
|
+
import { CalcuttaCountdown } from './CalcuttaCountdown';
|
|
15
|
+
import { AuctionInfoChips } from './AuctionInfoChips';
|
|
16
|
+
import { EscrowWidget } from './EscrowWidget';
|
|
17
|
+
import { AuctionCountdownOverlay } from './AuctionCountdownOverlay';
|
|
18
|
+
import { useCalcuttaSocket } from '../hooks/useCalcuttaSocket';
|
|
11
19
|
|
|
12
20
|
export interface CalcuttaDetailProps {
|
|
13
21
|
calcutta_competition_id: string;
|
|
14
22
|
player_id?: string;
|
|
15
|
-
|
|
23
|
+
access_token?: string;
|
|
24
|
+
device_id?: string;
|
|
25
|
+
player_username?: string;
|
|
26
|
+
player_profile_pic?: string;
|
|
27
|
+
onClose?: () => void;
|
|
16
28
|
onJoin?: () => Promise<void>;
|
|
17
29
|
onLeave?: () => Promise<void>;
|
|
18
30
|
onShare?: () => void;
|
|
19
31
|
onManage?: () => void;
|
|
32
|
+
player_balance?: number;
|
|
33
|
+
onDepositFunds?: (amount: number) => void;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
const getStatusConfig = (status: string) => {
|
|
@@ -38,8 +52,35 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
38
52
|
onLeave,
|
|
39
53
|
onShare,
|
|
40
54
|
onManage,
|
|
55
|
+
player_balance,
|
|
56
|
+
onDepositFunds,
|
|
57
|
+
access_token,
|
|
58
|
+
device_id,
|
|
59
|
+
player_username,
|
|
60
|
+
player_profile_pic,
|
|
41
61
|
}) => {
|
|
42
62
|
const { theme } = useTheme();
|
|
63
|
+
|
|
64
|
+
// Countdown overlay state
|
|
65
|
+
const [showCountdown, setShowCountdown] = useState(false);
|
|
66
|
+
const [countdownItem, setCountdownItem] = useState<any>(undefined);
|
|
67
|
+
|
|
68
|
+
// Socket for presence + auction start detection
|
|
69
|
+
const handleItemActive = useCallback((data: any) => {
|
|
70
|
+
// Auction just started — show countdown for ALL connected clients
|
|
71
|
+
setCountdownItem(data.item);
|
|
72
|
+
setShowCountdown(true);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const { connected: socketConnected, presence, socketState } = useCalcuttaSocket(
|
|
76
|
+
calcutta_competition_id,
|
|
77
|
+
access_token,
|
|
78
|
+
device_id,
|
|
79
|
+
{ username: player_username, profile_pic: player_profile_pic },
|
|
80
|
+
{ onItemActive: handleItemActive },
|
|
81
|
+
);
|
|
82
|
+
const onlinePlayers = presence.players;
|
|
83
|
+
const onlinePlayerIds = useMemo(() => new Set(onlinePlayers.map(p => p.player_id)), [onlinePlayers]);
|
|
43
84
|
const {
|
|
44
85
|
loading,
|
|
45
86
|
competition,
|
|
@@ -52,6 +93,9 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
52
93
|
|
|
53
94
|
const participantIds = useMemo(() => participants.map(p => p.player_id), [participants]);
|
|
54
95
|
const { players: enrichedPlayers } = useCalcuttaPlayers(participantIds);
|
|
96
|
+
const { escrow, fetchEscrow } = useCalcuttaEscrow(calcutta_competition_id);
|
|
97
|
+
const [escrowExpanded, setEscrowExpanded] = useState(false);
|
|
98
|
+
|
|
55
99
|
const { images: itemImages } = useCalcuttaItemImages(items);
|
|
56
100
|
|
|
57
101
|
const [joining, setJoining] = useState(false);
|
|
@@ -101,6 +145,8 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
101
145
|
const totalPot = Number(competition.total_pot) || 0;
|
|
102
146
|
const canLeave = hasJoined && !isSweepstakes && ['pending', 'scheduled'].includes(competition.status);
|
|
103
147
|
const heroImage = competition.image?.url;
|
|
148
|
+
const lifecycleState = deriveLifecycleState(competition.status, competition.auction_status, competition.auction_type);
|
|
149
|
+
const showEscrow = canManageEscrow(lifecycleState, competition.auction_type) && hasJoined;
|
|
104
150
|
|
|
105
151
|
// Sweepstakes: find the player's assigned item
|
|
106
152
|
const myParticipant = participants.find(p => p.player_id == player_id);
|
|
@@ -114,13 +160,15 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
114
160
|
const sections = [
|
|
115
161
|
{ key: 'hero' },
|
|
116
162
|
{ key: 'info' },
|
|
163
|
+
{ key: 'action-card' },
|
|
164
|
+
...(showEscrow ? [{ key: 'escrow' }] : []),
|
|
117
165
|
{ key: 'stats' },
|
|
118
|
-
{ key: 'cta' },
|
|
119
166
|
{ key: 'payouts' },
|
|
120
167
|
{ key: 'items-header' },
|
|
121
168
|
{ key: 'items' },
|
|
122
169
|
{ key: 'participants-header' },
|
|
123
170
|
{ key: 'participants' },
|
|
171
|
+
...(canLeave ? [{ key: 'leave' }] : []),
|
|
124
172
|
];
|
|
125
173
|
|
|
126
174
|
const renderSection = ({ item }: { item: { key: string } }) => {
|
|
@@ -145,6 +193,17 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
145
193
|
<Text variant="caption" color="tertiary" style={{ marginLeft: 8 }}>
|
|
146
194
|
{isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
|
|
147
195
|
</Text>
|
|
196
|
+
<View variant="transparent" style={{ flex: 1 }} />
|
|
197
|
+
{isAdmin && onManage && (
|
|
198
|
+
<TouchableOpacity
|
|
199
|
+
onPress={onManage}
|
|
200
|
+
style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.surface.elevated, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8, borderWidth: 1, borderColor: theme.colors.border.subtle }}
|
|
201
|
+
activeOpacity={0.7}
|
|
202
|
+
>
|
|
203
|
+
<Ionicons name="settings-outline" size={14} color={theme.colors.text.secondary} />
|
|
204
|
+
<Text variant="caption" color="secondary" style={{ marginLeft: 4 }}>Manage</Text>
|
|
205
|
+
</TouchableOpacity>
|
|
206
|
+
)}
|
|
148
207
|
</View>
|
|
149
208
|
<Text variant="h3" bold>{competition.competition_name}</Text>
|
|
150
209
|
{competition.competition_description ? (
|
|
@@ -156,6 +215,25 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
156
215
|
<Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 4 }}>{competition.competition_code}</Text>
|
|
157
216
|
</View>
|
|
158
217
|
) : null}
|
|
218
|
+
{competition.scheduled_datetime && (
|
|
219
|
+
<CalcuttaCountdown
|
|
220
|
+
targetDate={competition.scheduled_datetime}
|
|
221
|
+
label="Auction starts"
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
{competition.auction_type === 'sealed_bid' && competition.auction_end_datetime && (
|
|
225
|
+
<CalcuttaCountdown
|
|
226
|
+
targetDate={competition.auction_end_datetime}
|
|
227
|
+
label="Bidding closes"
|
|
228
|
+
/>
|
|
229
|
+
)}
|
|
230
|
+
<AuctionInfoChips competition={competition} />
|
|
231
|
+
{onShare && (
|
|
232
|
+
<TouchableOpacity onPress={onShare} style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', marginTop: 10 }}>
|
|
233
|
+
<Ionicons name="share-outline" size={14} color={theme.colors.text.tertiary} />
|
|
234
|
+
<Text variant="caption" color="tertiary" style={{ marginLeft: 4 }}>Share</Text>
|
|
235
|
+
</TouchableOpacity>
|
|
236
|
+
)}
|
|
159
237
|
</View>
|
|
160
238
|
);
|
|
161
239
|
|
|
@@ -188,132 +266,91 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
188
266
|
</View>
|
|
189
267
|
);
|
|
190
268
|
|
|
191
|
-
case '
|
|
269
|
+
case 'action-card':
|
|
192
270
|
return (
|
|
193
|
-
<View variant="transparent" style={
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
271
|
+
<View variant="transparent" style={{ paddingHorizontal: 16, paddingTop: 4, paddingBottom: 12 }}>
|
|
272
|
+
<CalcuttaActionCard
|
|
273
|
+
competition={competition}
|
|
274
|
+
lifecycleState={lifecycleState}
|
|
275
|
+
hasJoined={hasJoined}
|
|
276
|
+
escrowBalance={Number(escrow?.escrow_balance ?? 0)}
|
|
277
|
+
isAdmin={isAdmin}
|
|
278
|
+
onJoin={onJoin ? async () => {
|
|
279
|
+
setJoining(true);
|
|
280
|
+
try { await onJoin(); await refresh(); fetchEscrow(); } catch {}
|
|
281
|
+
setJoining(false);
|
|
282
|
+
} : undefined}
|
|
283
|
+
onDepositEscrow={undefined}
|
|
284
|
+
onManage={onManage}
|
|
285
|
+
onStartAuction={isAdmin && competition.status === 'scheduled' ? async () => {
|
|
286
|
+
setStartingComp(true);
|
|
287
|
+
try {
|
|
288
|
+
if (isSweepstakes) {
|
|
208
289
|
await startSweepstakesCompetition(calcutta_competition_id);
|
|
209
290
|
await refresh();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
)}
|
|
226
|
-
</TouchableOpacity>
|
|
227
|
-
)}
|
|
228
|
-
|
|
229
|
-
{/* Join button */}
|
|
230
|
-
{isJoinable && !hasJoined && onJoin && (
|
|
231
|
-
<TouchableOpacity
|
|
232
|
-
onPress={async () => {
|
|
233
|
-
setJoining(true);
|
|
234
|
-
try {
|
|
235
|
-
await onJoin();
|
|
236
|
-
await refresh();
|
|
237
|
-
} catch {}
|
|
238
|
-
setJoining(false);
|
|
239
|
-
}}
|
|
240
|
-
disabled={joining}
|
|
241
|
-
style={[styles.ctaButton, { backgroundColor: '#10B981', opacity: joining ? 0.7 : 1 }]}
|
|
242
|
-
>
|
|
243
|
-
{joining ? (
|
|
244
|
-
<ActivityIndicator size="small" color="#FFF" />
|
|
291
|
+
} else {
|
|
292
|
+
await startCalcuttaAuction(calcutta_competition_id);
|
|
293
|
+
// Socket will broadcast CALCUTTA_ITEM_ACTIVE which triggers countdown
|
|
294
|
+
// refresh happens after countdown completes
|
|
295
|
+
}
|
|
296
|
+
} catch {}
|
|
297
|
+
setStartingComp(false);
|
|
298
|
+
} : undefined}
|
|
299
|
+
joining={joining}
|
|
300
|
+
/>
|
|
301
|
+
{/* Sweepstakes: show assigned item below action card */}
|
|
302
|
+
{isSweepstakes && myAssignedItem && (
|
|
303
|
+
<View variant="transparent" style={[styles.assignedItemCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, marginTop: 10 }]}>
|
|
304
|
+
{resolveItemImageUrl(myAssignedItem.item_image) ? (
|
|
305
|
+
<Image source={{ uri: resolveItemImageUrl(myAssignedItem.item_image)! }} style={styles.assignedItemImage} resizeMode="cover" />
|
|
245
306
|
) : (
|
|
246
|
-
<
|
|
247
|
-
{
|
|
248
|
-
? (isFree ? 'Join Free — Get a Random Team!' : `Join — ${formatCurrency(entryFee, competition.market_type)}`)
|
|
249
|
-
: (isFree ? 'Join - Free!' : `Join - ${formatCurrency(entryFee, competition.market_type)}`)}
|
|
250
|
-
</Text>
|
|
251
|
-
)}
|
|
252
|
-
</TouchableOpacity>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
{/* Already joined badge + assigned item (sweepstakes) + leave */}
|
|
256
|
-
{hasJoined && (
|
|
257
|
-
<View variant="transparent" style={{ gap: 10 }}>
|
|
258
|
-
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
259
|
-
<View variant="transparent" style={[styles.joinedBadge, { flex: 1, backgroundColor: '#10B98115' }]}>
|
|
260
|
-
<Ionicons name="checkmark-circle" size={16} color="#10B981" />
|
|
261
|
-
<Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6 }}>You're in!</Text>
|
|
262
|
-
</View>
|
|
263
|
-
{onLeave && canLeave && (
|
|
264
|
-
<TouchableOpacity
|
|
265
|
-
onPress={async () => {
|
|
266
|
-
setLeaving(true);
|
|
267
|
-
try {
|
|
268
|
-
await onLeave();
|
|
269
|
-
await refresh();
|
|
270
|
-
} catch {}
|
|
271
|
-
setLeaving(false);
|
|
272
|
-
}}
|
|
273
|
-
disabled={leaving}
|
|
274
|
-
style={[styles.ctaButtonOutline, { borderColor: theme.colors.status.error, paddingHorizontal: 16, opacity: leaving ? 0.7 : 1 }]}
|
|
275
|
-
>
|
|
276
|
-
{leaving ? (
|
|
277
|
-
<ActivityIndicator size="small" color={theme.colors.status.error} />
|
|
278
|
-
) : (
|
|
279
|
-
<Text variant="caption" bold style={{ color: theme.colors.status.error }}>Leave</Text>
|
|
280
|
-
)}
|
|
281
|
-
</TouchableOpacity>
|
|
282
|
-
)}
|
|
283
|
-
</View>
|
|
284
|
-
{/* Sweepstakes: show assigned item */}
|
|
285
|
-
{isSweepstakes && myAssignedItem && (
|
|
286
|
-
<View variant="transparent" style={[styles.assignedItemCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
287
|
-
{resolveItemImageUrl(myAssignedItem.item_image) ? (
|
|
288
|
-
<Image
|
|
289
|
-
source={{ uri: resolveItemImageUrl(myAssignedItem.item_image)! }}
|
|
290
|
-
style={styles.assignedItemImage}
|
|
291
|
-
resizeMode="cover"
|
|
292
|
-
/>
|
|
293
|
-
) : (
|
|
294
|
-
<View variant="transparent" style={[styles.assignedItemImage, { backgroundColor: theme.colors.primary.subtle, alignItems: 'center', justifyContent: 'center' }]}>
|
|
295
|
-
<Ionicons name="trophy" size={20} color={theme.colors.primary.default} />
|
|
296
|
-
</View>
|
|
297
|
-
)}
|
|
298
|
-
<View variant="transparent" style={{ flex: 1, marginLeft: 12 }}>
|
|
299
|
-
<Text variant="caption" color="tertiary">Your Team</Text>
|
|
300
|
-
<Text variant="body" bold>{myAssignedItem.item_name}</Text>
|
|
301
|
-
{myAssignedItem.seed != null && (
|
|
302
|
-
<Text variant="caption" color="tertiary">Seed #{myAssignedItem.seed}</Text>
|
|
303
|
-
)}
|
|
304
|
-
</View>
|
|
307
|
+
<View variant="transparent" style={[styles.assignedItemImage, { backgroundColor: theme.colors.primary.subtle, alignItems: 'center', justifyContent: 'center' }]}>
|
|
308
|
+
<Ionicons name="trophy" size={20} color={theme.colors.primary.default} />
|
|
305
309
|
</View>
|
|
306
310
|
)}
|
|
311
|
+
<View variant="transparent" style={{ flex: 1, marginLeft: 12 }}>
|
|
312
|
+
<Text variant="caption" color="tertiary">Your Item</Text>
|
|
313
|
+
<Text variant="body" bold>{myAssignedItem.item_name}</Text>
|
|
314
|
+
{myAssignedItem.seed != null && <Text variant="caption" color="tertiary">Seed #{myAssignedItem.seed}</Text>}
|
|
315
|
+
</View>
|
|
307
316
|
</View>
|
|
308
317
|
)}
|
|
318
|
+
</View>
|
|
319
|
+
);
|
|
309
320
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
case 'escrow':
|
|
322
|
+
return (
|
|
323
|
+
<View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
|
|
324
|
+
<EscrowWidget
|
|
325
|
+
calcutta_competition_id={calcutta_competition_id}
|
|
326
|
+
market_type={competition.market_type}
|
|
327
|
+
max_escrow={competition.max_escrow}
|
|
328
|
+
player_balance={player_balance}
|
|
329
|
+
onUpdate={() => { refresh(); fetchEscrow(); }}
|
|
330
|
+
initialExpanded={Number(escrow?.escrow_balance ?? 0) <= 0}
|
|
331
|
+
/>
|
|
332
|
+
</View>
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
case 'leave':
|
|
336
|
+
return (
|
|
337
|
+
<View variant="transparent" style={{ alignItems: 'center', paddingVertical: 16, paddingBottom: 40 }}>
|
|
338
|
+
<TouchableOpacity
|
|
339
|
+
onPress={async () => {
|
|
340
|
+
if (!onLeave) return;
|
|
341
|
+
setLeaving(true);
|
|
342
|
+
try { await onLeave(); await refresh(); } catch {}
|
|
343
|
+
setLeaving(false);
|
|
344
|
+
}}
|
|
345
|
+
disabled={leaving}
|
|
346
|
+
style={{ opacity: leaving ? 0.5 : 1 }}
|
|
347
|
+
>
|
|
348
|
+
{leaving ? (
|
|
349
|
+
<ActivityIndicator size="small" color={theme.colors.status.error} />
|
|
350
|
+
) : (
|
|
351
|
+
<Text variant="caption" style={{ color: theme.colors.status.error }}>Leave Competition</Text>
|
|
352
|
+
)}
|
|
353
|
+
</TouchableOpacity>
|
|
317
354
|
</View>
|
|
318
355
|
);
|
|
319
356
|
|
|
@@ -392,6 +429,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
392
429
|
color: theme.colors.text.primary,
|
|
393
430
|
paddingHorizontal: 12,
|
|
394
431
|
fontSize: 14,
|
|
432
|
+
lineHeight: 19,
|
|
395
433
|
marginBottom: 8,
|
|
396
434
|
}}
|
|
397
435
|
placeholder="Search by team or owner..."
|
|
@@ -442,7 +480,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
442
480
|
{items.map(item => (
|
|
443
481
|
<View key={item.calcutta_auction_item_id} variant="transparent" style={[styles.itemChip, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
444
482
|
<Text variant="caption" bold numberOfLines={1}>{item.item_name}</Text>
|
|
445
|
-
{item.seed != null && <Text variant="caption" color="tertiary" style={{ fontSize: 10 }}>#{item.seed}</Text>}
|
|
483
|
+
{item.seed != null && <Text variant="caption" color="tertiary" style={{ fontSize: 10, lineHeight: 13 }}>#{item.seed}</Text>}
|
|
446
484
|
</View>
|
|
447
485
|
))}
|
|
448
486
|
</ScrollView>
|
|
@@ -452,9 +490,15 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
452
490
|
case 'participants-header':
|
|
453
491
|
return (
|
|
454
492
|
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle, paddingBottom: 0 }]}>
|
|
455
|
-
<
|
|
456
|
-
Players ({participants.length})
|
|
457
|
-
|
|
493
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
|
|
494
|
+
<Text variant="body" bold>Players ({participants.length})</Text>
|
|
495
|
+
{onlinePlayers.length > 0 && (
|
|
496
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginLeft: 10, backgroundColor: '#10B98115', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 10 }}>
|
|
497
|
+
<View variant="transparent" style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: '#10B981', marginRight: 4 }} />
|
|
498
|
+
<Text variant="caption" bold style={{ color: '#10B981' }}>{onlinePlayers.length} online</Text>
|
|
499
|
+
</View>
|
|
500
|
+
)}
|
|
501
|
+
</View>
|
|
458
502
|
</View>
|
|
459
503
|
);
|
|
460
504
|
|
|
@@ -473,17 +517,23 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
473
517
|
const enriched = enrichedPlayers[p.player_id];
|
|
474
518
|
const username = enriched?.username || enriched?.show_name || `Player ${p.player_id.slice(0, 6)}`;
|
|
475
519
|
const profilePic = enriched?.profile_pic;
|
|
520
|
+
const isOnline = onlinePlayerIds.has(p.player_id);
|
|
476
521
|
return (
|
|
477
522
|
<View key={p.calcutta_participant_id} variant="transparent" style={[styles.participantRow, i < participants.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
478
|
-
{
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
<
|
|
483
|
-
{
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
523
|
+
<View variant="transparent" style={{ position: 'relative' }}>
|
|
524
|
+
{profilePic ? (
|
|
525
|
+
<Image source={{ uri: profilePic }} style={[styles.avatarCircle, { overflow: 'hidden' }]} />
|
|
526
|
+
) : (
|
|
527
|
+
<View variant="transparent" style={[styles.avatarCircle, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
528
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
|
|
529
|
+
{username.charAt(0).toUpperCase()}
|
|
530
|
+
</Text>
|
|
531
|
+
</View>
|
|
532
|
+
)}
|
|
533
|
+
{isOnline && (
|
|
534
|
+
<View variant="transparent" style={{ position: 'absolute', bottom: 0, right: 0, width: 10, height: 10, borderRadius: 5, backgroundColor: '#10B981', borderWidth: 2, borderColor: theme.colors.surface.base }} />
|
|
535
|
+
)}
|
|
536
|
+
</View>
|
|
487
537
|
<View variant="transparent" style={{ flex: 1 }}>
|
|
488
538
|
<Text variant="body">{username}</Text>
|
|
489
539
|
<Text variant="caption" color="tertiary">
|
|
@@ -492,7 +542,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
492
542
|
</View>
|
|
493
543
|
{p.player_id == player_id && (
|
|
494
544
|
<View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
495
|
-
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10 }}>YOU</Text>
|
|
545
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10, lineHeight: 13 }}>YOU</Text>
|
|
496
546
|
</View>
|
|
497
547
|
)}
|
|
498
548
|
</View>
|
|
@@ -506,27 +556,44 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
506
556
|
}
|
|
507
557
|
};
|
|
508
558
|
|
|
559
|
+
const socketColor = socketState === 'authenticated' ? '#10B981'
|
|
560
|
+
: socketState === 'connected' ? '#F59E0B'
|
|
561
|
+
: socketState === 'connecting' ? '#3B82F6'
|
|
562
|
+
: '#EF4444';
|
|
563
|
+
|
|
509
564
|
return (
|
|
510
565
|
<View variant="base" style={styles.container}>
|
|
511
|
-
{/*
|
|
512
|
-
{
|
|
513
|
-
<View variant="transparent" style={
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
)}
|
|
566
|
+
{/* Socket debug bar */}
|
|
567
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 6, backgroundColor: socketColor + '15', borderBottomWidth: 1, borderColor: socketColor + '30' }}>
|
|
568
|
+
<View variant="transparent" style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: socketColor, marginRight: 6 }} />
|
|
569
|
+
<Text variant="caption" style={{ color: socketColor }}>
|
|
570
|
+
Socket: {socketState} {onlinePlayers.length > 0 ? `· ${onlinePlayers.length} in room` : ''}
|
|
571
|
+
</Text>
|
|
572
|
+
{!access_token && <Text variant="caption" style={{ color: '#EF4444', marginLeft: 8 }}>No auth token</Text>}
|
|
573
|
+
</View>
|
|
520
574
|
|
|
521
575
|
<FlatList
|
|
522
576
|
data={sections}
|
|
523
577
|
keyExtractor={(item) => item.key}
|
|
524
578
|
renderItem={renderSection}
|
|
579
|
+
extraData={`${escrowExpanded}-${onlinePlayers.length}`}
|
|
525
580
|
showsVerticalScrollIndicator={false}
|
|
526
581
|
style={{ backgroundColor: theme.colors.surface.base }}
|
|
527
582
|
contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40 }}
|
|
528
583
|
keyboardShouldPersistTaps="handled"
|
|
529
584
|
/>
|
|
585
|
+
|
|
586
|
+
{/* Auction starting countdown overlay */}
|
|
587
|
+
<AuctionCountdownOverlay
|
|
588
|
+
visible={showCountdown}
|
|
589
|
+
competitionName={competition?.competition_name}
|
|
590
|
+
firstItem={countdownItem}
|
|
591
|
+
countdownFrom={5}
|
|
592
|
+
onComplete={() => {
|
|
593
|
+
setShowCountdown(false);
|
|
594
|
+
refresh();
|
|
595
|
+
}}
|
|
596
|
+
/>
|
|
530
597
|
</View>
|
|
531
598
|
);
|
|
532
599
|
};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 ||
|
|
308
|
+
: (loading || isInvalidAmount)
|
|
284
309
|
? theme.colors.surface.elevated
|
|
285
310
|
: theme.colors.primary.default,
|
|
286
311
|
}]}
|
|
287
312
|
onPress={handleAction}
|
|
288
|
-
disabled={loading ||
|
|
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={
|
|
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:
|
|
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
|
-
{/*
|
|
320
|
-
{
|
|
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
|
-
{/*
|
|
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
|
},
|