@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.
- package/package.json +46 -0
- package/src/components/CalcuttaAuction.tsx +453 -0
- package/src/components/CalcuttaAuctionItem.tsx +292 -0
- package/src/components/CalcuttaBidInput.tsx +214 -0
- package/src/components/CalcuttaCard.tsx +131 -0
- package/src/components/CalcuttaDetail.tsx +377 -0
- package/src/components/CalcuttaEscrow.tsx +464 -0
- package/src/components/CalcuttaItemResults.tsx +207 -0
- package/src/components/CalcuttaLeaderboard.tsx +179 -0
- package/src/components/CalcuttaPayoutPreview.tsx +194 -0
- package/src/components/CalcuttaRoundResults.tsx +250 -0
- package/src/components/CalcuttaTemplateSelector.tsx +124 -0
- package/src/components/sealed/AuctionResultsModal.tsx +165 -0
- package/src/components/sealed/EscrowBottomSheet.tsx +185 -0
- package/src/components/sealed/SealedBidAuction.tsx +541 -0
- package/src/components/sealed/SealedBidHeader.tsx +116 -0
- package/src/components/sealed/SealedBidInfoTab.tsx +247 -0
- package/src/components/sealed/SealedBidItemCard.tsx +385 -0
- package/src/components/sealed/SealedBidItemsTab.tsx +235 -0
- package/src/components/sealed/SealedBidMyBidsTab.tsx +512 -0
- package/src/components/sealed/SealedBidPlayersTab.tsx +220 -0
- package/src/components/sealed/SealedBidStatusBar.tsx +415 -0
- package/src/components/sealed/SealedBidTabBar.tsx +172 -0
- package/src/helpers/formatting.ts +56 -0
- package/src/helpers/lifecycleState.ts +71 -0
- package/src/helpers/payout.ts +39 -0
- package/src/helpers/validation.ts +64 -0
- package/src/hooks/useCalcuttaAuction.ts +164 -0
- package/src/hooks/useCalcuttaBid.ts +43 -0
- package/src/hooks/useCalcuttaCompetition.ts +63 -0
- package/src/hooks/useCalcuttaEscrow.ts +52 -0
- package/src/hooks/useCalcuttaItemImages.ts +79 -0
- package/src/hooks/useCalcuttaPlayers.ts +46 -0
- package/src/hooks/useCalcuttaResults.ts +58 -0
- package/src/hooks/useCalcuttaSocket.ts +131 -0
- package/src/hooks/useCalcuttaTemplates.ts +36 -0
- package/src/index.ts +74 -0
- package/src/types.ts +31 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
4
|
+
import { View, Text, useTheme, toast } from '@bettoredge/styles';
|
|
5
|
+
import { useCalcuttaAuction } from '../../hooks/useCalcuttaAuction';
|
|
6
|
+
import { useCalcuttaEscrow } from '../../hooks/useCalcuttaEscrow';
|
|
7
|
+
import { useCalcuttaBid } from '../../hooks/useCalcuttaBid';
|
|
8
|
+
import { useCalcuttaSocket } from '../../hooks/useCalcuttaSocket';
|
|
9
|
+
import { useCalcuttaCompetition } from '../../hooks/useCalcuttaCompetition';
|
|
10
|
+
import { useCalcuttaPlayers } from '../../hooks/useCalcuttaPlayers';
|
|
11
|
+
import { useCalcuttaItemImages } from '../../hooks/useCalcuttaItemImages';
|
|
12
|
+
import { deriveLifecycleState, canBid as canBidCheck, type CalcuttaLifecycleState } from '../../helpers/lifecycleState';
|
|
13
|
+
import { formatCurrency } from '../../helpers/formatting';
|
|
14
|
+
import { SealedBidHeader } from './SealedBidHeader';
|
|
15
|
+
import { SealedBidStatusBar } from './SealedBidStatusBar';
|
|
16
|
+
import { SealedBidTabBar, SealedBidTab } from './SealedBidTabBar';
|
|
17
|
+
import { SealedBidItemsTab } from './SealedBidItemsTab';
|
|
18
|
+
import { SealedBidMyBidsTab } from './SealedBidMyBidsTab';
|
|
19
|
+
import { SealedBidPlayersTab } from './SealedBidPlayersTab';
|
|
20
|
+
import { SealedBidInfoTab } from './SealedBidInfoTab';
|
|
21
|
+
import { EscrowBottomSheet } from './EscrowBottomSheet';
|
|
22
|
+
import { AuctionResultsModal } from './AuctionResultsModal';
|
|
23
|
+
import { CalcuttaRoundResults } from '../CalcuttaRoundResults';
|
|
24
|
+
import { CalcuttaLeaderboard } from '../CalcuttaLeaderboard';
|
|
25
|
+
|
|
26
|
+
interface SealedBidAuctionProps {
|
|
27
|
+
calcutta_competition_id: string;
|
|
28
|
+
player_id?: string;
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
getSocket?: () => WebSocket | null;
|
|
31
|
+
getAccessToken?: () => string | undefined;
|
|
32
|
+
getDeviceId?: () => string | undefined;
|
|
33
|
+
player_data?: { username: string; profile_pic?: string };
|
|
34
|
+
player_balance?: number;
|
|
35
|
+
onDepositFunds?: (amount: number) => void;
|
|
36
|
+
onManage?: () => void;
|
|
37
|
+
onJoin?: () => void | Promise<void>;
|
|
38
|
+
onLeave?: () => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Poll interval per lifecycle state */
|
|
42
|
+
function getPollInterval(state: CalcuttaLifecycleState, auctionExpired: boolean): number {
|
|
43
|
+
// When timer expired but server hasn't confirmed close, poll aggressively
|
|
44
|
+
if (auctionExpired && state === 'auctioning') return 3000;
|
|
45
|
+
switch (state) {
|
|
46
|
+
case 'auctioning': return 10000;
|
|
47
|
+
case 'tournament': return 60000;
|
|
48
|
+
default: return 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SealedBidAuction: React.FC<SealedBidAuctionProps> = ({
|
|
53
|
+
calcutta_competition_id,
|
|
54
|
+
player_id,
|
|
55
|
+
onClose,
|
|
56
|
+
getSocket,
|
|
57
|
+
getAccessToken,
|
|
58
|
+
getDeviceId,
|
|
59
|
+
player_data,
|
|
60
|
+
player_balance,
|
|
61
|
+
onDepositFunds,
|
|
62
|
+
onManage,
|
|
63
|
+
onJoin,
|
|
64
|
+
onLeave,
|
|
65
|
+
}) => {
|
|
66
|
+
const { theme } = useTheme();
|
|
67
|
+
const [activeTab, setActiveTab] = useState<SealedBidTab>('items');
|
|
68
|
+
const [escrowSheetVisible, setEscrowSheetVisible] = useState(false);
|
|
69
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
70
|
+
const [showResultsModal, setShowResultsModal] = useState(false);
|
|
71
|
+
|
|
72
|
+
// Client-side flag: auction end time has passed.
|
|
73
|
+
// Does NOT change lifecycle state — only disables bid actions.
|
|
74
|
+
// Lifecycle only changes when the SERVER confirms auction_status='closed'.
|
|
75
|
+
const [auctionExpired, setAuctionExpired] = useState(false);
|
|
76
|
+
|
|
77
|
+
// Track previous lifecycle state to detect transitions
|
|
78
|
+
const prevLifecycleRef = useRef<CalcuttaLifecycleState | undefined>(undefined);
|
|
79
|
+
const hasShownResultsRef = useRef(false);
|
|
80
|
+
|
|
81
|
+
// Competition detail (rounds, participants, payout rules, item_results)
|
|
82
|
+
const {
|
|
83
|
+
rounds,
|
|
84
|
+
participants,
|
|
85
|
+
payout_rules,
|
|
86
|
+
item_results,
|
|
87
|
+
refresh: refreshCompetition,
|
|
88
|
+
} = useCalcuttaCompetition(calcutta_competition_id);
|
|
89
|
+
|
|
90
|
+
// Auction data (items, bids)
|
|
91
|
+
const {
|
|
92
|
+
loading,
|
|
93
|
+
competition,
|
|
94
|
+
items,
|
|
95
|
+
my_bids,
|
|
96
|
+
refresh: refreshAuction,
|
|
97
|
+
} = useCalcuttaAuction(calcutta_competition_id, 10000, player_id);
|
|
98
|
+
|
|
99
|
+
// Client-side check: has the auction end time passed?
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!competition?.auction_end_datetime) return;
|
|
102
|
+
const end = new Date(competition.auction_end_datetime).getTime();
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
|
|
105
|
+
if (now >= end) {
|
|
106
|
+
setAuctionExpired(true);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Set a timer to flip the flag the instant the deadline passes
|
|
111
|
+
const delay = end - now;
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
setAuctionExpired(true);
|
|
114
|
+
// Immediately poll the server for fresh status
|
|
115
|
+
refreshAuction();
|
|
116
|
+
refreshCompetition();
|
|
117
|
+
}, delay);
|
|
118
|
+
|
|
119
|
+
return () => clearTimeout(timer);
|
|
120
|
+
}, [competition?.auction_end_datetime, refreshAuction, refreshCompetition]);
|
|
121
|
+
|
|
122
|
+
// Derive lifecycle state — only from server data, no client override
|
|
123
|
+
const lifecycleState = deriveLifecycleState(
|
|
124
|
+
competition?.status,
|
|
125
|
+
competition?.auction_status,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Bidding is disabled when lifecycle says no OR when the timer has expired
|
|
129
|
+
const biddingDisabled = !canBidCheck(lifecycleState) || auctionExpired;
|
|
130
|
+
|
|
131
|
+
// Escrow
|
|
132
|
+
const { escrow, fetchEscrow } = useCalcuttaEscrow(calcutta_competition_id);
|
|
133
|
+
|
|
134
|
+
// Bid actions
|
|
135
|
+
const { placeBid, cancelBid, loading: bidLoading } = useCalcuttaBid();
|
|
136
|
+
|
|
137
|
+
// Player enrichment
|
|
138
|
+
const participantIds = useMemo(
|
|
139
|
+
() => participants.map(p => p.player_id),
|
|
140
|
+
[participants],
|
|
141
|
+
);
|
|
142
|
+
const { players } = useCalcuttaPlayers(participantIds);
|
|
143
|
+
|
|
144
|
+
// Item images (team logos / athlete photos)
|
|
145
|
+
const { images: itemImages } = useCalcuttaItemImages(items);
|
|
146
|
+
|
|
147
|
+
// Socket for presence
|
|
148
|
+
const noopSocket = useCallback(() => null, []);
|
|
149
|
+
const noopString = useCallback(() => undefined, []);
|
|
150
|
+
const { connected: socketConnected, presence } = useCalcuttaSocket(
|
|
151
|
+
calcutta_competition_id,
|
|
152
|
+
player_data || { username: '' },
|
|
153
|
+
getSocket || noopSocket,
|
|
154
|
+
getAccessToken || noopString,
|
|
155
|
+
getDeviceId || noopString,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Adaptive polling based on lifecycle state
|
|
159
|
+
const pollInterval = getPollInterval(lifecycleState, auctionExpired);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (pollInterval <= 0) return;
|
|
162
|
+
const id = setInterval(() => {
|
|
163
|
+
refreshAuction();
|
|
164
|
+
refreshCompetition();
|
|
165
|
+
}, pollInterval);
|
|
166
|
+
return () => clearInterval(id);
|
|
167
|
+
}, [pollInterval, refreshAuction, refreshCompetition]);
|
|
168
|
+
|
|
169
|
+
// Detect auctioning → tournament transition → show results modal
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!competition || hasShownResultsRef.current) return;
|
|
172
|
+
const prev = prevLifecycleRef.current;
|
|
173
|
+
const curr = lifecycleState;
|
|
174
|
+
|
|
175
|
+
const justTransitioned = prev === 'auctioning' && (curr === 'tournament' || curr === 'completed');
|
|
176
|
+
|
|
177
|
+
if (justTransitioned) {
|
|
178
|
+
setShowResultsModal(true);
|
|
179
|
+
hasShownResultsRef.current = true;
|
|
180
|
+
}
|
|
181
|
+
prevLifecycleRef.current = curr;
|
|
182
|
+
}, [lifecycleState, competition]);
|
|
183
|
+
|
|
184
|
+
// Auto-switch tab when state changes
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
const prev = prevLifecycleRef.current;
|
|
187
|
+
if (!prev) return;
|
|
188
|
+
|
|
189
|
+
if (lifecycleState === 'tournament' || lifecycleState === 'completed') {
|
|
190
|
+
setActiveTab('my_items');
|
|
191
|
+
} else if (lifecycleState === 'auctioning') {
|
|
192
|
+
setActiveTab('items');
|
|
193
|
+
}
|
|
194
|
+
}, [lifecycleState]);
|
|
195
|
+
|
|
196
|
+
// On first load, set the correct default tab for the current state
|
|
197
|
+
const hasSetInitialTab = useRef(false);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (hasSetInitialTab.current || !competition) return;
|
|
200
|
+
hasSetInitialTab.current = true;
|
|
201
|
+
|
|
202
|
+
if (lifecycleState === 'tournament' || lifecycleState === 'completed') {
|
|
203
|
+
setActiveTab('my_items');
|
|
204
|
+
}
|
|
205
|
+
}, [lifecycleState, competition]);
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
fetchEscrow();
|
|
209
|
+
}, [fetchEscrow]);
|
|
210
|
+
|
|
211
|
+
const handlePlaceBid = useCallback(async (calcutta_auction_item_id: string, amount: number) => {
|
|
212
|
+
if (biddingDisabled) return;
|
|
213
|
+
try {
|
|
214
|
+
await placeBid(calcutta_auction_item_id, amount);
|
|
215
|
+
fetchEscrow();
|
|
216
|
+
refreshAuction();
|
|
217
|
+
toast.success('Bid placed');
|
|
218
|
+
} catch (e: any) {
|
|
219
|
+
toast.error(e?.message || 'Failed to place bid');
|
|
220
|
+
}
|
|
221
|
+
}, [placeBid, fetchEscrow, refreshAuction, biddingDisabled]);
|
|
222
|
+
|
|
223
|
+
const handleCancelBid = useCallback(async (calcutta_bid_id: string) => {
|
|
224
|
+
if (biddingDisabled) return;
|
|
225
|
+
try {
|
|
226
|
+
await cancelBid(calcutta_bid_id);
|
|
227
|
+
fetchEscrow();
|
|
228
|
+
refreshAuction();
|
|
229
|
+
toast.success('Bid cancelled');
|
|
230
|
+
} catch (e: any) {
|
|
231
|
+
toast.error(e?.message || 'Failed to cancel bid');
|
|
232
|
+
}
|
|
233
|
+
}, [cancelBid, fetchEscrow, refreshAuction, biddingDisabled]);
|
|
234
|
+
|
|
235
|
+
const handleUpdateBid = useCallback((calcutta_auction_item_id: string) => {
|
|
236
|
+
if (biddingDisabled) return;
|
|
237
|
+
setActiveTab('items');
|
|
238
|
+
}, [biddingDisabled]);
|
|
239
|
+
|
|
240
|
+
const handleRefresh = useCallback(async () => {
|
|
241
|
+
setRefreshing(true);
|
|
242
|
+
await Promise.all([refreshAuction(), refreshCompetition(), fetchEscrow()]);
|
|
243
|
+
setRefreshing(false);
|
|
244
|
+
}, [refreshAuction, refreshCompetition, fetchEscrow]);
|
|
245
|
+
|
|
246
|
+
const handleEscrowUpdate = useCallback(() => {
|
|
247
|
+
fetchEscrow();
|
|
248
|
+
}, [fetchEscrow]);
|
|
249
|
+
|
|
250
|
+
const handleResultsDismiss = useCallback(() => {
|
|
251
|
+
setShowResultsModal(false);
|
|
252
|
+
setActiveTab('my_items');
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
const [joining, setJoining] = useState(false);
|
|
256
|
+
const handleJoin = useCallback(async () => {
|
|
257
|
+
if (!onJoin) return;
|
|
258
|
+
setJoining(true);
|
|
259
|
+
try {
|
|
260
|
+
await onJoin();
|
|
261
|
+
await Promise.all([refreshAuction(), refreshCompetition(), fetchEscrow()]);
|
|
262
|
+
toast.success('Joined auction');
|
|
263
|
+
} catch (e: any) {
|
|
264
|
+
toast.error(e?.message || 'Failed to join auction');
|
|
265
|
+
}
|
|
266
|
+
setJoining(false);
|
|
267
|
+
}, [onJoin, refreshAuction, refreshCompetition, fetchEscrow]);
|
|
268
|
+
|
|
269
|
+
const [leaving, setLeaving] = useState(false);
|
|
270
|
+
const handleLeave = useCallback(async () => {
|
|
271
|
+
if (!onLeave) return;
|
|
272
|
+
setLeaving(true);
|
|
273
|
+
try {
|
|
274
|
+
await onLeave();
|
|
275
|
+
await Promise.all([refreshAuction(), refreshCompetition(), fetchEscrow()]);
|
|
276
|
+
toast.success('Left auction');
|
|
277
|
+
} catch (e: any) {
|
|
278
|
+
toast.error(e?.message || 'Failed to leave auction');
|
|
279
|
+
}
|
|
280
|
+
setLeaving(false);
|
|
281
|
+
}, [onLeave, refreshAuction, refreshCompetition, fetchEscrow]);
|
|
282
|
+
|
|
283
|
+
if (loading && !competition) {
|
|
284
|
+
return (
|
|
285
|
+
<View variant="transparent" style={styles.loadingContainer}>
|
|
286
|
+
<ActivityIndicator size="large" color={theme.colors.primary.default} />
|
|
287
|
+
</View>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!competition) {
|
|
292
|
+
return (
|
|
293
|
+
<View variant="transparent" style={styles.loadingContainer}>
|
|
294
|
+
<Text variant="body" color="secondary">Auction not found</Text>
|
|
295
|
+
</View>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const isAdmin = player_id != null && competition.admin_id === player_id;
|
|
300
|
+
const isParticipant = player_id != null && participants.some(p => p.player_id === player_id);
|
|
301
|
+
|
|
302
|
+
// All items I purchased at auction (includes active + eliminated)
|
|
303
|
+
const myPurchasedItems = (lifecycleState === 'tournament' || lifecycleState === 'completed')
|
|
304
|
+
? items.filter(i => i.winning_player_id === player_id)
|
|
305
|
+
: [];
|
|
306
|
+
// Only active (non-eliminated) for the results modal
|
|
307
|
+
const wonItems = myPurchasedItems.filter(i => i.status === 'sold');
|
|
308
|
+
const myBidTabCount = (lifecycleState === 'tournament' || lifecycleState === 'completed')
|
|
309
|
+
? myPurchasedItems.length
|
|
310
|
+
: my_bids.filter(b => b.bid_status === 'active').length;
|
|
311
|
+
|
|
312
|
+
const statusBar = (
|
|
313
|
+
<SealedBidStatusBar
|
|
314
|
+
competition={competition}
|
|
315
|
+
escrow={escrow}
|
|
316
|
+
my_bids={my_bids}
|
|
317
|
+
items={items}
|
|
318
|
+
totalItems={items.length}
|
|
319
|
+
market_type={competition.market_type}
|
|
320
|
+
player_id={player_id}
|
|
321
|
+
lifecycleState={lifecycleState}
|
|
322
|
+
auctionExpired={auctionExpired}
|
|
323
|
+
isParticipant={isParticipant}
|
|
324
|
+
onEscrowTap={() => setEscrowSheetVisible(true)}
|
|
325
|
+
onLeave={isParticipant && onLeave && lifecycleState === 'auctioning' ? handleLeave : undefined}
|
|
326
|
+
leaving={leaving}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<View variant="transparent" style={styles.container}>
|
|
332
|
+
<SealedBidHeader
|
|
333
|
+
competition={competition}
|
|
334
|
+
isAdmin={isAdmin}
|
|
335
|
+
onClose={onClose}
|
|
336
|
+
onManage={isAdmin ? onManage : undefined}
|
|
337
|
+
lifecycleState={lifecycleState}
|
|
338
|
+
auctionExpired={auctionExpired}
|
|
339
|
+
/>
|
|
340
|
+
|
|
341
|
+
<SealedBidTabBar
|
|
342
|
+
activeTab={activeTab}
|
|
343
|
+
onTabChange={setActiveTab}
|
|
344
|
+
itemCount={items.length}
|
|
345
|
+
myBidCount={myBidTabCount}
|
|
346
|
+
playerCount={participants.length}
|
|
347
|
+
lifecycleState={lifecycleState}
|
|
348
|
+
/>
|
|
349
|
+
|
|
350
|
+
{/* Join banner for non-participants */}
|
|
351
|
+
{!isParticipant && onJoin && (
|
|
352
|
+
<View variant="transparent" style={[styles.joinBanner, { backgroundColor: theme.colors.primary.subtle, borderColor: theme.colors.primary.default + '30' }]}>
|
|
353
|
+
<View variant="transparent" style={styles.joinInfo}>
|
|
354
|
+
<Ionicons name="enter-outline" size={20} color={theme.colors.primary.default} />
|
|
355
|
+
<View variant="transparent" style={{ marginLeft: 10, flex: 1 }}>
|
|
356
|
+
<Text variant="body" bold>Join this auction</Text>
|
|
357
|
+
<Text variant="caption" color="secondary">
|
|
358
|
+
Join to deposit escrow and start placing bids
|
|
359
|
+
{competition.entry_fee > 0 ? ` · ${formatCurrency(competition.entry_fee, competition.market_type)} entry fee` : ''}
|
|
360
|
+
</Text>
|
|
361
|
+
</View>
|
|
362
|
+
</View>
|
|
363
|
+
<TouchableOpacity
|
|
364
|
+
style={[styles.joinBtn, { backgroundColor: theme.colors.primary.default, opacity: joining ? 0.7 : 1 }]}
|
|
365
|
+
onPress={handleJoin}
|
|
366
|
+
disabled={joining}
|
|
367
|
+
activeOpacity={0.7}
|
|
368
|
+
>
|
|
369
|
+
{joining ? (
|
|
370
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
371
|
+
) : (
|
|
372
|
+
<Text variant="body" bold style={{ color: '#FFFFFF' }}>Join</Text>
|
|
373
|
+
)}
|
|
374
|
+
</TouchableOpacity>
|
|
375
|
+
</View>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{/* Tab content */}
|
|
379
|
+
<View variant="transparent" style={styles.tabContent}>
|
|
380
|
+
{activeTab === 'items' && (
|
|
381
|
+
<SealedBidItemsTab
|
|
382
|
+
items={items}
|
|
383
|
+
my_bids={my_bids}
|
|
384
|
+
min_bid={competition.min_bid}
|
|
385
|
+
bid_increment={competition.bid_increment}
|
|
386
|
+
escrow_balance={escrow?.escrow_balance ?? 0}
|
|
387
|
+
market_type={competition.market_type}
|
|
388
|
+
player_id={player_id}
|
|
389
|
+
lifecycleState={lifecycleState}
|
|
390
|
+
itemImages={itemImages}
|
|
391
|
+
onPlaceBid={handlePlaceBid}
|
|
392
|
+
bidLoading={bidLoading}
|
|
393
|
+
onRefresh={handleRefresh}
|
|
394
|
+
refreshing={refreshing}
|
|
395
|
+
disabled={biddingDisabled}
|
|
396
|
+
listHeader={statusBar}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
399
|
+
{activeTab === 'my_bids' && (
|
|
400
|
+
<SealedBidMyBidsTab
|
|
401
|
+
items={items}
|
|
402
|
+
my_bids={my_bids}
|
|
403
|
+
escrow={escrow}
|
|
404
|
+
market_type={competition.market_type}
|
|
405
|
+
player_id={player_id}
|
|
406
|
+
lifecycleState={lifecycleState}
|
|
407
|
+
itemImages={itemImages}
|
|
408
|
+
item_results={item_results}
|
|
409
|
+
rounds={rounds}
|
|
410
|
+
onCancelBid={handleCancelBid}
|
|
411
|
+
onUpdateBid={handleUpdateBid}
|
|
412
|
+
cancelLoading={bidLoading}
|
|
413
|
+
auctionExpired={auctionExpired}
|
|
414
|
+
listHeader={statusBar}
|
|
415
|
+
/>
|
|
416
|
+
)}
|
|
417
|
+
{activeTab === 'my_items' && (
|
|
418
|
+
<SealedBidMyBidsTab
|
|
419
|
+
items={items}
|
|
420
|
+
my_bids={my_bids}
|
|
421
|
+
escrow={escrow}
|
|
422
|
+
market_type={competition.market_type}
|
|
423
|
+
player_id={player_id}
|
|
424
|
+
lifecycleState={lifecycleState}
|
|
425
|
+
itemImages={itemImages}
|
|
426
|
+
item_results={item_results}
|
|
427
|
+
rounds={rounds}
|
|
428
|
+
onCancelBid={handleCancelBid}
|
|
429
|
+
onUpdateBid={handleUpdateBid}
|
|
430
|
+
cancelLoading={bidLoading}
|
|
431
|
+
auctionExpired={auctionExpired}
|
|
432
|
+
listHeader={statusBar}
|
|
433
|
+
/>
|
|
434
|
+
)}
|
|
435
|
+
{activeTab === 'players' && (
|
|
436
|
+
<SealedBidPlayersTab
|
|
437
|
+
participants={participants}
|
|
438
|
+
players={players}
|
|
439
|
+
player_id={player_id}
|
|
440
|
+
presence={presence}
|
|
441
|
+
socketConnected={socketConnected}
|
|
442
|
+
market_type={competition.market_type}
|
|
443
|
+
lifecycleState={lifecycleState}
|
|
444
|
+
listHeader={statusBar}
|
|
445
|
+
/>
|
|
446
|
+
)}
|
|
447
|
+
{activeTab === 'results' && (
|
|
448
|
+
<CalcuttaRoundResults
|
|
449
|
+
rounds={rounds}
|
|
450
|
+
items={items}
|
|
451
|
+
item_results={item_results}
|
|
452
|
+
payout_rules={payout_rules}
|
|
453
|
+
market_type={competition.market_type}
|
|
454
|
+
total_pot={Number(competition.total_pot) || 0}
|
|
455
|
+
unclaimed_pot={Number(competition.unclaimed_pot) || 0}
|
|
456
|
+
/>
|
|
457
|
+
)}
|
|
458
|
+
{activeTab === 'leaderboard' && (
|
|
459
|
+
<CalcuttaLeaderboard
|
|
460
|
+
participants={participants}
|
|
461
|
+
players={players}
|
|
462
|
+
player_id={player_id}
|
|
463
|
+
market_type={competition.market_type}
|
|
464
|
+
listHeader={statusBar}
|
|
465
|
+
/>
|
|
466
|
+
)}
|
|
467
|
+
{activeTab === 'info' && (
|
|
468
|
+
<SealedBidInfoTab
|
|
469
|
+
competition={competition}
|
|
470
|
+
rounds={rounds}
|
|
471
|
+
payout_rules={payout_rules}
|
|
472
|
+
escrow={escrow}
|
|
473
|
+
market_type={competition.market_type}
|
|
474
|
+
player_balance={player_balance}
|
|
475
|
+
onDepositFunds={onDepositFunds}
|
|
476
|
+
onEscrowUpdate={handleEscrowUpdate}
|
|
477
|
+
lifecycleState={lifecycleState}
|
|
478
|
+
/>
|
|
479
|
+
)}
|
|
480
|
+
</View>
|
|
481
|
+
|
|
482
|
+
{/* Escrow bottom sheet */}
|
|
483
|
+
<EscrowBottomSheet
|
|
484
|
+
visible={escrowSheetVisible}
|
|
485
|
+
onDismiss={() => setEscrowSheetVisible(false)}
|
|
486
|
+
calcutta_competition_id={calcutta_competition_id}
|
|
487
|
+
escrow={escrow}
|
|
488
|
+
market_type={competition.market_type}
|
|
489
|
+
max_escrow={competition.max_escrow}
|
|
490
|
+
player_balance={player_balance}
|
|
491
|
+
onDepositFunds={onDepositFunds}
|
|
492
|
+
lifecycleState={lifecycleState}
|
|
493
|
+
onUpdate={() => {
|
|
494
|
+
handleEscrowUpdate();
|
|
495
|
+
setEscrowSheetVisible(false);
|
|
496
|
+
}}
|
|
497
|
+
/>
|
|
498
|
+
|
|
499
|
+
{/* Auction results celebration modal */}
|
|
500
|
+
<AuctionResultsModal
|
|
501
|
+
visible={showResultsModal}
|
|
502
|
+
onDismiss={handleResultsDismiss}
|
|
503
|
+
wonItems={wonItems}
|
|
504
|
+
market_type={competition.market_type}
|
|
505
|
+
/>
|
|
506
|
+
</View>
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const styles = StyleSheet.create({
|
|
511
|
+
container: {
|
|
512
|
+
flex: 1,
|
|
513
|
+
},
|
|
514
|
+
loadingContainer: {
|
|
515
|
+
flex: 1,
|
|
516
|
+
alignItems: 'center',
|
|
517
|
+
justifyContent: 'center',
|
|
518
|
+
padding: 40,
|
|
519
|
+
},
|
|
520
|
+
joinBanner: {
|
|
521
|
+
marginHorizontal: 12,
|
|
522
|
+
marginTop: 10,
|
|
523
|
+
padding: 14,
|
|
524
|
+
borderRadius: 12,
|
|
525
|
+
borderWidth: 1,
|
|
526
|
+
},
|
|
527
|
+
joinInfo: {
|
|
528
|
+
flexDirection: 'row',
|
|
529
|
+
alignItems: 'center',
|
|
530
|
+
},
|
|
531
|
+
joinBtn: {
|
|
532
|
+
marginTop: 12,
|
|
533
|
+
height: 44,
|
|
534
|
+
borderRadius: 10,
|
|
535
|
+
alignItems: 'center',
|
|
536
|
+
justifyContent: 'center',
|
|
537
|
+
},
|
|
538
|
+
tabContent: {
|
|
539
|
+
flex: 1,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
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 } from '@bettoredge/types';
|
|
6
|
+
import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
|
|
7
|
+
|
|
8
|
+
interface SealedBidHeaderProps {
|
|
9
|
+
competition: CalcuttaCompetitionProps;
|
|
10
|
+
isAdmin: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onManage?: () => void;
|
|
13
|
+
lifecycleState: CalcuttaLifecycleState;
|
|
14
|
+
auctionExpired?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getStateBadge(state: CalcuttaLifecycleState): { label: string; color: string; bg: string } {
|
|
18
|
+
switch (state) {
|
|
19
|
+
case 'pending':
|
|
20
|
+
return { label: 'Coming Soon', color: '#F59E0B', bg: '#F59E0B15' };
|
|
21
|
+
case 'scheduled':
|
|
22
|
+
return { label: 'Open to Join', color: '#3B82F6', bg: '#3B82F615' };
|
|
23
|
+
case 'auctioning':
|
|
24
|
+
return { label: 'Live', color: '#10B981', bg: '#10B98115' };
|
|
25
|
+
case 'tournament':
|
|
26
|
+
return { label: 'In Progress', color: '#8B5CF6', bg: '#8B5CF615' };
|
|
27
|
+
case 'completed':
|
|
28
|
+
return { label: 'Completed', color: '#6B7280', bg: '#6B728015' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const SealedBidHeader: React.FC<SealedBidHeaderProps> = ({
|
|
33
|
+
competition,
|
|
34
|
+
isAdmin,
|
|
35
|
+
onManage,
|
|
36
|
+
lifecycleState,
|
|
37
|
+
auctionExpired,
|
|
38
|
+
}) => {
|
|
39
|
+
const { theme } = useTheme();
|
|
40
|
+
const badge = (auctionExpired && lifecycleState === 'auctioning')
|
|
41
|
+
? { label: 'Closing...', color: '#F59E0B', bg: '#F59E0B15' }
|
|
42
|
+
: getStateBadge(lifecycleState);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View variant="transparent" style={[styles.container, { borderColor: theme.colors.border.subtle }]}>
|
|
46
|
+
{competition.image?.url ? (
|
|
47
|
+
<Image source={{ uri: competition.image.url }} style={styles.thumb} />
|
|
48
|
+
) : (
|
|
49
|
+
<View variant="transparent" style={[styles.thumb, { backgroundColor: theme.colors.surface.elevated }]}>
|
|
50
|
+
<Ionicons name="hammer-outline" size={20} color={theme.colors.text.tertiary} />
|
|
51
|
+
</View>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<View variant="transparent" style={styles.titleWrap}>
|
|
55
|
+
<Text variant="body" bold numberOfLines={1}>{competition.competition_name}</Text>
|
|
56
|
+
<View variant="transparent" style={styles.badgeRow}>
|
|
57
|
+
<View variant="transparent" style={[styles.badge, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
58
|
+
<Text variant="caption" style={{ color: theme.colors.primary.default, fontSize: 10 }}>Sealed Bid</Text>
|
|
59
|
+
</View>
|
|
60
|
+
<View variant="transparent" style={[styles.badge, { backgroundColor: badge.bg, marginLeft: 4 }]}>
|
|
61
|
+
<Text variant="caption" style={{ color: badge.color, fontSize: 10 }}>{badge.label}</Text>
|
|
62
|
+
</View>
|
|
63
|
+
</View>
|
|
64
|
+
</View>
|
|
65
|
+
|
|
66
|
+
{isAdmin && onManage && (
|
|
67
|
+
<TouchableOpacity
|
|
68
|
+
onPress={onManage}
|
|
69
|
+
style={[styles.gearBtn, { backgroundColor: theme.colors.surface.elevated }]}
|
|
70
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
71
|
+
>
|
|
72
|
+
<Ionicons name="settings-outline" size={18} color={theme.colors.text.secondary} />
|
|
73
|
+
</TouchableOpacity>
|
|
74
|
+
)}
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const styles = StyleSheet.create({
|
|
80
|
+
container: {
|
|
81
|
+
flexDirection: 'row',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
paddingHorizontal: 14,
|
|
84
|
+
paddingVertical: 10,
|
|
85
|
+
borderBottomWidth: 1,
|
|
86
|
+
},
|
|
87
|
+
thumb: {
|
|
88
|
+
width: 44,
|
|
89
|
+
height: 44,
|
|
90
|
+
borderRadius: 10,
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
overflow: 'hidden',
|
|
94
|
+
marginRight: 10,
|
|
95
|
+
},
|
|
96
|
+
titleWrap: {
|
|
97
|
+
flex: 1,
|
|
98
|
+
},
|
|
99
|
+
badgeRow: {
|
|
100
|
+
flexDirection: 'row',
|
|
101
|
+
marginTop: 2,
|
|
102
|
+
},
|
|
103
|
+
badge: {
|
|
104
|
+
paddingHorizontal: 6,
|
|
105
|
+
paddingVertical: 2,
|
|
106
|
+
borderRadius: 4,
|
|
107
|
+
},
|
|
108
|
+
gearBtn: {
|
|
109
|
+
width: 32,
|
|
110
|
+
height: 32,
|
|
111
|
+
borderRadius: 16,
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
marginLeft: 8,
|
|
115
|
+
},
|
|
116
|
+
});
|