@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,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
+ });