@bettoredge/calcutta 0.2.0 → 0.3.1
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 +4 -4
- package/src/components/CalcuttaAuction.tsx +310 -3
- package/src/components/CalcuttaCard.tsx +13 -4
- package/src/components/CalcuttaDetail.tsx +226 -44
- package/src/components/CalcuttaPayoutPreview.tsx +54 -2
- package/src/components/CalcuttaRoundResults.tsx +23 -5
- package/src/components/SweepstakesReveal.tsx +178 -0
- package/src/helpers/formatting.ts +9 -0
- package/src/helpers/lifecycleState.ts +17 -11
- package/src/hooks/useCalcuttaResults.ts +6 -0
- package/src/index.ts +4 -1
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bettoredge/calcutta",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Calcutta auction competition components for BettorEdge applications",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@bettoredge/styles": "^0.4.
|
|
32
|
+
"@bettoredge/styles": "^0.4.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/react": "^19.0.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"react": ">=18.3.1",
|
|
41
41
|
"react-native": ">=0.76.5",
|
|
42
42
|
"@expo/vector-icons": "*",
|
|
43
|
-
"@bettoredge/api": ">=0.8.
|
|
44
|
-
"@bettoredge/types": ">=0.5.
|
|
43
|
+
"@bettoredge/api": ">=0.8.3",
|
|
44
|
+
"@bettoredge/types": ">=0.5.2"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
2
|
-
import { StyleSheet, TouchableOpacity, ActivityIndicator, FlatList, ScrollView } from 'react-native';
|
|
1
|
+
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity, ActivityIndicator, FlatList, ScrollView, Image, TextInput, Platform } from 'react-native';
|
|
3
3
|
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
4
|
import { Ionicons } from '@expo/vector-icons';
|
|
5
5
|
import type { CalcuttaEscrowProps } from '@bettoredge/types';
|
|
@@ -9,7 +9,8 @@ import { useCalcuttaEscrow } from '../hooks/useCalcuttaEscrow';
|
|
|
9
9
|
import { useCalcuttaBid } from '../hooks/useCalcuttaBid';
|
|
10
10
|
import { useCalcuttaSocket, CalcuttaPresence, CalcuttaPresencePlayer } from '../hooks/useCalcuttaSocket';
|
|
11
11
|
import { useCalcuttaItemImages } from '../hooks/useCalcuttaItemImages';
|
|
12
|
-
import {
|
|
12
|
+
import { useCalcuttaPlayers } from '../hooks/useCalcuttaPlayers';
|
|
13
|
+
import { formatCurrency, getStatusLabel, resolveItemImageUrl } from '../helpers/formatting';
|
|
13
14
|
import { CalcuttaAuctionItem } from './CalcuttaAuctionItem';
|
|
14
15
|
import { CalcuttaEscrow } from './CalcuttaEscrow';
|
|
15
16
|
import { SealedBidAuction } from './sealed/SealedBidAuction';
|
|
@@ -334,6 +335,308 @@ const LiveAuction: React.FC<CalcuttaAuctionProps> = ({
|
|
|
334
335
|
);
|
|
335
336
|
};
|
|
336
337
|
|
|
338
|
+
// Sweepstakes: no auction, show the player's assigned item and prize structure
|
|
339
|
+
const SweepstakesWaiting: React.FC<CalcuttaAuctionProps & { competition: any }> = ({
|
|
340
|
+
calcutta_competition_id,
|
|
341
|
+
player_id,
|
|
342
|
+
onClose,
|
|
343
|
+
onManage,
|
|
344
|
+
competition,
|
|
345
|
+
}) => {
|
|
346
|
+
const { theme } = useTheme();
|
|
347
|
+
const { items, rounds, participants, item_results } = useCalcuttaCompetition(calcutta_competition_id);
|
|
348
|
+
const { images: itemImages } = useCalcuttaItemImages(items);
|
|
349
|
+
const isAdmin = player_id != null && competition?.admin_id == player_id;
|
|
350
|
+
|
|
351
|
+
// Build player lookup for owner usernames
|
|
352
|
+
const ownerIds = items.filter(i => i.winning_player_id).map(i => i.winning_player_id!);
|
|
353
|
+
const { players: enrichedPlayers } = useCalcuttaPlayers(ownerIds);
|
|
354
|
+
|
|
355
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
356
|
+
|
|
357
|
+
const [expandedRound, setExpandedRound] = useState<string | null>(null);
|
|
358
|
+
|
|
359
|
+
const myItem = items.find(i => i.winning_player_id == player_id);
|
|
360
|
+
const sortedRounds = [...(rounds ?? [])].sort((a, b) => a.round_number - b.round_number);
|
|
361
|
+
|
|
362
|
+
const filteredItems = useMemo(() => {
|
|
363
|
+
if (!searchQuery.trim()) return items;
|
|
364
|
+
const q = searchQuery.toLowerCase().trim();
|
|
365
|
+
return items.filter(item => {
|
|
366
|
+
if (item.item_name.toLowerCase().includes(q)) return true;
|
|
367
|
+
if (item.winning_player_id) {
|
|
368
|
+
const owner = enrichedPlayers[item.winning_player_id];
|
|
369
|
+
const ownerName = (owner?.username || owner?.show_name || '').toLowerCase();
|
|
370
|
+
if (ownerName.includes(q)) return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
});
|
|
374
|
+
}, [items, searchQuery, enrichedPlayers]);
|
|
375
|
+
const statusLabel = competition.status === 'inprogress' ? 'In Progress'
|
|
376
|
+
: competition.status === 'closed' ? 'Completed' : 'Scheduled';
|
|
377
|
+
|
|
378
|
+
const statusColor = competition.status === 'inprogress' ? '#8B5CF6'
|
|
379
|
+
: competition.status === 'closed' ? '#6B7280'
|
|
380
|
+
: '#10B981';
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<View variant="base" style={styles.container}>
|
|
384
|
+
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ gap: 16, paddingBottom: Platform.OS === 'web' ? 300 : 40 }} keyboardShouldPersistTaps="handled">
|
|
385
|
+
{/* Hero with embedded title */}
|
|
386
|
+
<View style={{ position: 'relative' }}>
|
|
387
|
+
{competition.image?.url ? (
|
|
388
|
+
<Image source={{ uri: competition.image.url }} style={{ width: '100%', aspectRatio: 16 / 9 }} resizeMode="cover" />
|
|
389
|
+
) : (
|
|
390
|
+
<View variant="transparent" style={{ width: '100%', aspectRatio: 16 / 9, backgroundColor: '#1e1b4b' }} />
|
|
391
|
+
)}
|
|
392
|
+
{/* Dark overlay for text readability */}
|
|
393
|
+
<View variant="transparent" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: '70%', backgroundColor: 'rgba(0,0,0,0.5)' }} />
|
|
394
|
+
{/* Top bar: status badge + manage */}
|
|
395
|
+
<View variant="transparent" style={{ position: 'absolute', top: 12, left: 16, right: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
396
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: statusColor + '30', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, gap: 6 }}>
|
|
397
|
+
<View variant="transparent" style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: statusColor }} />
|
|
398
|
+
<Text variant="caption" bold style={{ color: statusColor }}>{statusLabel}</Text>
|
|
399
|
+
</View>
|
|
400
|
+
{isAdmin && onManage && (
|
|
401
|
+
<TouchableOpacity
|
|
402
|
+
onPress={onManage}
|
|
403
|
+
activeOpacity={0.7}
|
|
404
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 6, backgroundColor: 'rgba(0,0,0,0.55)', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8, borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)' }}
|
|
405
|
+
>
|
|
406
|
+
<Ionicons name="settings-outline" size={14} color="#fff" />
|
|
407
|
+
<Text variant="caption" bold style={{ color: '#fff' }}>Manage</Text>
|
|
408
|
+
</TouchableOpacity>
|
|
409
|
+
)}
|
|
410
|
+
</View>
|
|
411
|
+
{/* Title overlay */}
|
|
412
|
+
<View variant="transparent" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, paddingHorizontal: 20, paddingBottom: 28 }}>
|
|
413
|
+
<Text variant="h2" bold style={{ color: '#fff', fontSize: 26, lineHeight: 34, fontWeight: '800', letterSpacing: -0.5 }}>
|
|
414
|
+
{competition.competition_name}
|
|
415
|
+
</Text>
|
|
416
|
+
<Text variant="caption" style={{ color: 'rgba(255,255,255,0.7)', marginTop: 4 }}>
|
|
417
|
+
Sweepstakes
|
|
418
|
+
</Text>
|
|
419
|
+
</View>
|
|
420
|
+
</View>
|
|
421
|
+
|
|
422
|
+
{/* Assigned item */}
|
|
423
|
+
{myItem && (() => {
|
|
424
|
+
const myItemImageUrl = resolveItemImageUrl(myItem.item_image) || itemImages[myItem.item_id]?.url;
|
|
425
|
+
return (
|
|
426
|
+
<View variant="transparent" style={[styles.rulesCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderRadius: 10, marginHorizontal: 16 }]}>
|
|
427
|
+
{myItemImageUrl ? (
|
|
428
|
+
<Image source={{ uri: myItemImageUrl }} style={{ width: 40, height: 40, borderRadius: 8 }} resizeMode="cover" />
|
|
429
|
+
) : (
|
|
430
|
+
<Ionicons name="trophy" size={24} color={theme.colors.primary.default} />
|
|
431
|
+
)}
|
|
432
|
+
<View variant="transparent" style={{ marginLeft: 12, flex: 1 }}>
|
|
433
|
+
<Text variant="caption" color="tertiary">Your Team</Text>
|
|
434
|
+
<Text variant="body" bold>{myItem.item_name}</Text>
|
|
435
|
+
{myItem.seed != null && <Text variant="caption" color="tertiary">Seed #{myItem.seed}</Text>}
|
|
436
|
+
</View>
|
|
437
|
+
</View>
|
|
438
|
+
);
|
|
439
|
+
})()}
|
|
440
|
+
|
|
441
|
+
{/* Stats */}
|
|
442
|
+
<View variant="transparent" style={{ flexDirection: 'row', gap: 12, paddingHorizontal: 16 }}>
|
|
443
|
+
<View variant="transparent" style={[styles.rulesCard, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderRadius: 10, alignItems: 'center' }]}>
|
|
444
|
+
<Text variant="caption" color="tertiary">Players</Text>
|
|
445
|
+
<Text variant="body" bold>{participants.length}</Text>
|
|
446
|
+
</View>
|
|
447
|
+
<View variant="transparent" style={[styles.rulesCard, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderRadius: 10, alignItems: 'center' }]}>
|
|
448
|
+
<Text variant="caption" color="tertiary">Teams</Text>
|
|
449
|
+
<Text variant="body" bold>{items.length}</Text>
|
|
450
|
+
</View>
|
|
451
|
+
<View variant="transparent" style={[styles.rulesCard, { flex: 1, backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle, borderRadius: 10, alignItems: 'center' }]}>
|
|
452
|
+
<Text variant="caption" color="tertiary">Rounds</Text>
|
|
453
|
+
<Text variant="body" bold>{rounds.length}</Text>
|
|
454
|
+
</View>
|
|
455
|
+
</View>
|
|
456
|
+
|
|
457
|
+
{/* Rounds with results */}
|
|
458
|
+
{sortedRounds.length > 0 && (
|
|
459
|
+
<View variant="transparent" style={{ paddingHorizontal: 16 }}>
|
|
460
|
+
<Text variant="body" bold style={{ marginBottom: 10 }}>Rounds</Text>
|
|
461
|
+
{sortedRounds.map((round: any) => {
|
|
462
|
+
const isExpanded = expandedRound === round.calcutta_round_id;
|
|
463
|
+
const roundResults = item_results.filter(r => r.round_number === round.round_number);
|
|
464
|
+
const advancedIds = roundResults.filter(r => r.result === 'advanced' || r.result === 'won').map(r => r.calcutta_auction_item_id);
|
|
465
|
+
const eliminatedIds = roundResults.filter(r => r.result === 'eliminated').map(r => r.calcutta_auction_item_id);
|
|
466
|
+
const advancedItems = items.filter(i => advancedIds.includes(i.calcutta_auction_item_id));
|
|
467
|
+
const eliminatedItems = items.filter(i => eliminatedIds.includes(i.calcutta_auction_item_id));
|
|
468
|
+
const hasResults = roundResults.length > 0;
|
|
469
|
+
const statusColor = round.status === 'closed' ? theme.colors.status.success
|
|
470
|
+
: round.status === 'inprogress' ? theme.colors.primary.default
|
|
471
|
+
: theme.colors.text.tertiary;
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<View key={round.calcutta_round_id} variant="transparent" style={{ borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
|
|
475
|
+
<TouchableOpacity
|
|
476
|
+
onPress={() => setExpandedRound(isExpanded ? null : round.calcutta_round_id)}
|
|
477
|
+
activeOpacity={0.7}
|
|
478
|
+
style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 12 }}
|
|
479
|
+
>
|
|
480
|
+
<View variant="transparent" style={{ flex: 1 }}>
|
|
481
|
+
<Text variant="body" bold>{round.round_name}</Text>
|
|
482
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginTop: 4, gap: 8 }}>
|
|
483
|
+
<View variant="transparent" style={{ width: 6, height: 6, borderRadius: 3, backgroundColor: statusColor }} />
|
|
484
|
+
<Text variant="caption" color="tertiary">
|
|
485
|
+
{round.status === 'closed' ? 'Resolved' : round.status === 'inprogress' ? 'In Progress' : 'Pending'}
|
|
486
|
+
</Text>
|
|
487
|
+
{hasResults && (
|
|
488
|
+
<Text variant="caption" style={{ color: theme.colors.status.success }}>
|
|
489
|
+
{advancedItems.length} advanced
|
|
490
|
+
</Text>
|
|
491
|
+
)}
|
|
492
|
+
</View>
|
|
493
|
+
{round.prize_description && (
|
|
494
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', marginTop: 4 }}>
|
|
495
|
+
<Ionicons name="gift-outline" size={12} color={theme.colors.primary.default} />
|
|
496
|
+
<Text variant="caption" style={{ color: theme.colors.primary.default, marginLeft: 4 }}>{round.prize_description}</Text>
|
|
497
|
+
</View>
|
|
498
|
+
)}
|
|
499
|
+
</View>
|
|
500
|
+
<Ionicons name={isExpanded ? 'chevron-up' : 'chevron-down'} size={16} color={theme.colors.text.tertiary} />
|
|
501
|
+
</TouchableOpacity>
|
|
502
|
+
|
|
503
|
+
{isExpanded && (
|
|
504
|
+
<View variant="transparent" style={{ paddingBottom: 12 }}>
|
|
505
|
+
{!hasResults && (
|
|
506
|
+
<Text variant="caption" color="tertiary" style={{ paddingVertical: 8 }}>No results yet</Text>
|
|
507
|
+
)}
|
|
508
|
+
{advancedItems.length > 0 && (
|
|
509
|
+
<>
|
|
510
|
+
<Text variant="caption" bold color="secondary" style={{ marginTop: 4, marginBottom: 4 }}>Advanced ({advancedItems.length})</Text>
|
|
511
|
+
{advancedItems.map(item => {
|
|
512
|
+
const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
|
|
513
|
+
const isMe = item.winning_player_id == player_id;
|
|
514
|
+
return (
|
|
515
|
+
<View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 4, paddingLeft: 4 }}>
|
|
516
|
+
{imgUrl ? (
|
|
517
|
+
<Image source={{ uri: imgUrl }} style={{ width: 20, height: 20, borderRadius: 4 }} resizeMode="cover" />
|
|
518
|
+
) : (
|
|
519
|
+
<Ionicons name="checkmark-circle" size={16} color={theme.colors.status.success} />
|
|
520
|
+
)}
|
|
521
|
+
<Text variant="caption" style={{ marginLeft: 6, flex: 1 }}>{item.item_name}</Text>
|
|
522
|
+
{isMe && <Text variant="caption" bold style={{ color: theme.colors.primary.default }}>YOU</Text>}
|
|
523
|
+
</View>
|
|
524
|
+
);
|
|
525
|
+
})}
|
|
526
|
+
</>
|
|
527
|
+
)}
|
|
528
|
+
{eliminatedItems.length > 0 && (
|
|
529
|
+
<>
|
|
530
|
+
<Text variant="caption" bold color="secondary" style={{ marginTop: 8, marginBottom: 4 }}>Eliminated ({eliminatedItems.length})</Text>
|
|
531
|
+
{eliminatedItems.map(item => {
|
|
532
|
+
const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
|
|
533
|
+
const isMe = item.winning_player_id == player_id;
|
|
534
|
+
return (
|
|
535
|
+
<View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 4, paddingLeft: 4, opacity: 0.5 }}>
|
|
536
|
+
{imgUrl ? (
|
|
537
|
+
<Image source={{ uri: imgUrl }} style={{ width: 20, height: 20, borderRadius: 4 }} resizeMode="cover" />
|
|
538
|
+
) : (
|
|
539
|
+
<Ionicons name="close-circle" size={16} color={theme.colors.status.error} />
|
|
540
|
+
)}
|
|
541
|
+
<Text variant="caption" style={{ marginLeft: 6, flex: 1 }}>{item.item_name}</Text>
|
|
542
|
+
{isMe && <Text variant="caption" bold style={{ color: theme.colors.status.error }}>YOU</Text>}
|
|
543
|
+
</View>
|
|
544
|
+
);
|
|
545
|
+
})}
|
|
546
|
+
</>
|
|
547
|
+
)}
|
|
548
|
+
</View>
|
|
549
|
+
)}
|
|
550
|
+
</View>
|
|
551
|
+
);
|
|
552
|
+
})}
|
|
553
|
+
</View>
|
|
554
|
+
)}
|
|
555
|
+
|
|
556
|
+
{/* All teams list */}
|
|
557
|
+
{items.length > 0 && (
|
|
558
|
+
<View variant="transparent" style={{ paddingHorizontal: 16 }}>
|
|
559
|
+
<Text variant="body" bold style={{ marginBottom: 10 }}>All Teams</Text>
|
|
560
|
+
<TextInput
|
|
561
|
+
style={{
|
|
562
|
+
height: 36,
|
|
563
|
+
borderRadius: 8,
|
|
564
|
+
borderWidth: 1,
|
|
565
|
+
borderColor: theme.colors.border.subtle,
|
|
566
|
+
backgroundColor: theme.colors.surface.input,
|
|
567
|
+
color: theme.colors.text.primary,
|
|
568
|
+
paddingHorizontal: 12,
|
|
569
|
+
fontSize: 14,
|
|
570
|
+
marginBottom: 8,
|
|
571
|
+
}}
|
|
572
|
+
placeholder="Search by team or owner..."
|
|
573
|
+
placeholderTextColor={theme.colors.text.tertiary}
|
|
574
|
+
value={searchQuery}
|
|
575
|
+
onChangeText={setSearchQuery}
|
|
576
|
+
autoCapitalize="none"
|
|
577
|
+
autoCorrect={false}
|
|
578
|
+
onFocus={(e: any) => {
|
|
579
|
+
if (Platform.OS === 'web' && e?.target?.scrollIntoView) {
|
|
580
|
+
setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
|
|
581
|
+
}
|
|
582
|
+
}}
|
|
583
|
+
/>
|
|
584
|
+
{filteredItems.map((item) => {
|
|
585
|
+
const isMyItem = item.winning_player_id == player_id;
|
|
586
|
+
const isEliminated = item.status === 'eliminated';
|
|
587
|
+
const imageUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
|
|
588
|
+
const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
|
|
589
|
+
const ownerName = owner?.username || owner?.show_name;
|
|
590
|
+
return (
|
|
591
|
+
<View
|
|
592
|
+
key={item.calcutta_auction_item_id}
|
|
593
|
+
variant="transparent"
|
|
594
|
+
style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle, opacity: isEliminated ? 0.5 : 1 }}
|
|
595
|
+
>
|
|
596
|
+
{imageUrl ? (
|
|
597
|
+
<Image source={{ uri: imageUrl }} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="cover" />
|
|
598
|
+
) : (
|
|
599
|
+
<Ionicons
|
|
600
|
+
name={isEliminated ? 'close-circle' : isMyItem ? 'star' : 'ellipse-outline'}
|
|
601
|
+
size={16}
|
|
602
|
+
color={isEliminated ? theme.colors.status.error : isMyItem ? theme.colors.primary.default : theme.colors.text.tertiary}
|
|
603
|
+
/>
|
|
604
|
+
)}
|
|
605
|
+
<View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
|
|
606
|
+
<Text variant="body">{item.item_name}</Text>
|
|
607
|
+
{ownerName && (
|
|
608
|
+
<Text variant="caption" color="tertiary">
|
|
609
|
+
{isMyItem ? 'You' : ownerName}
|
|
610
|
+
</Text>
|
|
611
|
+
)}
|
|
612
|
+
{!item.winning_player_id && item.status !== 'unsold' && (
|
|
613
|
+
<Text variant="caption" color="tertiary">Unclaimed</Text>
|
|
614
|
+
)}
|
|
615
|
+
{item.status === 'unsold' && (
|
|
616
|
+
<Text variant="caption" color="tertiary">No owner</Text>
|
|
617
|
+
)}
|
|
618
|
+
</View>
|
|
619
|
+
{item.seed != null && <Text variant="caption" color="tertiary">#{item.seed}</Text>}
|
|
620
|
+
{isMyItem && <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 8 }}>YOU</Text>}
|
|
621
|
+
</View>
|
|
622
|
+
);
|
|
623
|
+
})}
|
|
624
|
+
</View>
|
|
625
|
+
)}
|
|
626
|
+
|
|
627
|
+
{!myItem && !isAdmin && (
|
|
628
|
+
<View variant="transparent" style={{ alignItems: 'center', paddingVertical: 20, paddingHorizontal: 16 }}>
|
|
629
|
+
<Ionicons name="time-outline" size={32} color={theme.colors.text.tertiary} />
|
|
630
|
+
<Text variant="body" color="tertiary" style={{ marginTop: 8 }}>
|
|
631
|
+
Waiting for the tournament to begin
|
|
632
|
+
</Text>
|
|
633
|
+
</View>
|
|
634
|
+
)}
|
|
635
|
+
</ScrollView>
|
|
636
|
+
</View>
|
|
637
|
+
);
|
|
638
|
+
};
|
|
639
|
+
|
|
337
640
|
// Main exported component — routes to sealed bid or live layout
|
|
338
641
|
export const CalcuttaAuction: React.FC<CalcuttaAuctionProps> = (props) => {
|
|
339
642
|
const { theme } = useTheme();
|
|
@@ -358,6 +661,10 @@ export const CalcuttaAuction: React.FC<CalcuttaAuctionProps> = (props) => {
|
|
|
358
661
|
);
|
|
359
662
|
}
|
|
360
663
|
|
|
664
|
+
if (competition.auction_type === 'sweepstakes') {
|
|
665
|
+
return <SweepstakesWaiting {...props} competition={competition} />;
|
|
666
|
+
}
|
|
667
|
+
|
|
361
668
|
if (competition.auction_type === 'sealed_bid') {
|
|
362
669
|
return <SealedBidAuction {...props} />;
|
|
363
670
|
}
|
|
@@ -19,7 +19,10 @@ export const CalcuttaCard: React.FC<CalcuttaCardProps> = ({
|
|
|
19
19
|
const { theme } = useTheme();
|
|
20
20
|
|
|
21
21
|
const statusLabel = getStatusLabel(competition.status);
|
|
22
|
-
const auctionLabel = competition.auction_type === '
|
|
22
|
+
const auctionLabel = competition.auction_type === 'sweepstakes'
|
|
23
|
+
? 'Sweepstakes'
|
|
24
|
+
: competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid';
|
|
25
|
+
const isSweepstakes = competition.auction_type === 'sweepstakes';
|
|
23
26
|
const participantCount = competition.participants?.length ?? 0;
|
|
24
27
|
|
|
25
28
|
return (
|
|
@@ -57,9 +60,15 @@ export const CalcuttaCard: React.FC<CalcuttaCardProps> = ({
|
|
|
57
60
|
</Text>
|
|
58
61
|
</View>
|
|
59
62
|
<View variant="transparent" style={styles.meta}>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
{isSweepstakes ? (
|
|
64
|
+
<Text variant="caption" color="secondary">
|
|
65
|
+
{competition.rounds?.filter(r => r.prize_description).length ?? 0} Prizes
|
|
66
|
+
</Text>
|
|
67
|
+
) : (
|
|
68
|
+
<Text variant="caption" color="secondary">
|
|
69
|
+
Pot: {formatCurrency(competition.total_pot, competition.market_type)}
|
|
70
|
+
</Text>
|
|
71
|
+
)}
|
|
63
72
|
<Text variant="caption" color="tertiary"> {'\u00B7'} </Text>
|
|
64
73
|
<Text variant="caption" color="secondary">
|
|
65
74
|
{participantCount} participant{participantCount !== 1 ? 's' : ''}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import React, { useState, useMemo } from 'react';
|
|
2
|
-
import { StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Image, FlatList } from 'react-native';
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
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';
|
|
5
5
|
import type { CalcuttaParticipantProps } from '@bettoredge/types';
|
|
6
6
|
import { useCalcuttaCompetition } from '../hooks/useCalcuttaCompetition';
|
|
7
7
|
import { useCalcuttaPlayers } from '../hooks/useCalcuttaPlayers';
|
|
8
|
-
import {
|
|
8
|
+
import { useCalcuttaItemImages } from '../hooks/useCalcuttaItemImages';
|
|
9
|
+
import { formatCurrency, getStatusLabel, resolveItemImageUrl } from '../helpers/formatting';
|
|
10
|
+
import { startSweepstakesCompetition } from '@bettoredge/api';
|
|
9
11
|
|
|
10
12
|
export interface CalcuttaDetailProps {
|
|
11
13
|
calcutta_competition_id: string;
|
|
@@ -50,9 +52,26 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
50
52
|
|
|
51
53
|
const participantIds = useMemo(() => participants.map(p => p.player_id), [participants]);
|
|
52
54
|
const { players: enrichedPlayers } = useCalcuttaPlayers(participantIds);
|
|
55
|
+
const { images: itemImages } = useCalcuttaItemImages(items);
|
|
53
56
|
|
|
54
57
|
const [joining, setJoining] = useState(false);
|
|
55
58
|
const [leaving, setLeaving] = useState(false);
|
|
59
|
+
const [startingComp, setStartingComp] = useState(false);
|
|
60
|
+
const [itemSearch, setItemSearch] = useState('');
|
|
61
|
+
|
|
62
|
+
const filteredItems = useMemo(() => {
|
|
63
|
+
if (competition?.auction_type !== 'sweepstakes' || !itemSearch.trim()) return items;
|
|
64
|
+
const q = itemSearch.toLowerCase().trim();
|
|
65
|
+
return items.filter(item => {
|
|
66
|
+
if (item.item_name.toLowerCase().includes(q)) return true;
|
|
67
|
+
if (item.winning_player_id) {
|
|
68
|
+
const owner = enrichedPlayers[item.winning_player_id];
|
|
69
|
+
const ownerName = (owner?.username || owner?.show_name || '').toLowerCase();
|
|
70
|
+
if (ownerName.includes(q)) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
}, [items, itemSearch, enrichedPlayers, competition?.auction_type]);
|
|
56
75
|
|
|
57
76
|
if (loading && !competition) {
|
|
58
77
|
return (
|
|
@@ -75,11 +94,23 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
75
94
|
const hasJoined = participants.some(p => p.player_id == player_id);
|
|
76
95
|
const entryFee = Number(competition.entry_fee) || 0;
|
|
77
96
|
const isFree = entryFee === 0;
|
|
78
|
-
const
|
|
97
|
+
const isSweepstakes = competition.auction_type === 'sweepstakes';
|
|
98
|
+
const isJoinable = isSweepstakes
|
|
99
|
+
? competition.status === 'scheduled'
|
|
100
|
+
: ['scheduled', 'auction_open'].includes(competition.status);
|
|
79
101
|
const totalPot = Number(competition.total_pot) || 0;
|
|
80
|
-
const canLeave = hasJoined && ['pending', 'scheduled'].includes(competition.status);
|
|
102
|
+
const canLeave = hasJoined && !isSweepstakes && ['pending', 'scheduled'].includes(competition.status);
|
|
81
103
|
const heroImage = competition.image?.url;
|
|
82
104
|
|
|
105
|
+
// Sweepstakes: find the player's assigned item
|
|
106
|
+
const myParticipant = participants.find(p => p.player_id == player_id);
|
|
107
|
+
const myAssignedItem = isSweepstakes && hasJoined
|
|
108
|
+
? items.find(i => i.winning_player_id == player_id)
|
|
109
|
+
: undefined;
|
|
110
|
+
const unassignedItemCount = isSweepstakes
|
|
111
|
+
? items.filter(i => !i.winning_player_id && i.status === 'pending').length
|
|
112
|
+
: 0;
|
|
113
|
+
|
|
83
114
|
const sections = [
|
|
84
115
|
{ key: 'hero' },
|
|
85
116
|
{ key: 'info' },
|
|
@@ -112,7 +143,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
112
143
|
<Text variant="caption" bold style={{ color: statusConfig.color }}>{statusConfig.label}</Text>
|
|
113
144
|
</View>
|
|
114
145
|
<Text variant="caption" color="tertiary" style={{ marginLeft: 8 }}>
|
|
115
|
-
{competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
|
|
146
|
+
{isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
|
|
116
147
|
</Text>
|
|
117
148
|
</View>
|
|
118
149
|
<Text variant="h3" bold>{competition.competition_name}</Text>
|
|
@@ -135,17 +166,24 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
135
166
|
<Text variant="caption" color="tertiary">Entry Fee</Text>
|
|
136
167
|
<Text variant="body" bold>{isFree ? 'FREE' : formatCurrency(entryFee, competition.market_type)}</Text>
|
|
137
168
|
</View>
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
{isSweepstakes ? (
|
|
170
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
171
|
+
<Text variant="caption" color="tertiary">Available</Text>
|
|
172
|
+
<Text variant="body" bold>{unassignedItemCount}/{items.length}</Text>
|
|
173
|
+
</View>
|
|
174
|
+
) : (
|
|
175
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
176
|
+
<Text variant="caption" color="tertiary">Pot</Text>
|
|
177
|
+
<Text variant="body" bold>{formatCurrency(totalPot, competition.market_type)}</Text>
|
|
178
|
+
</View>
|
|
179
|
+
)}
|
|
142
180
|
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
143
181
|
<Text variant="caption" color="tertiary">Players</Text>
|
|
144
182
|
<Text variant="body" bold>{participants.length}{competition.max_participants > 0 ? `/${competition.max_participants}` : ''}</Text>
|
|
145
183
|
</View>
|
|
146
184
|
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
147
|
-
<Text variant="caption" color="tertiary">Items</Text>
|
|
148
|
-
<Text variant="body" bold>{items.length}</Text>
|
|
185
|
+
<Text variant="caption" color="tertiary">{isSweepstakes ? 'Prizes' : 'Items'}</Text>
|
|
186
|
+
<Text variant="body" bold>{isSweepstakes ? rounds.filter(r => r.prize_description).length : items.length}</Text>
|
|
149
187
|
</View>
|
|
150
188
|
</View>
|
|
151
189
|
);
|
|
@@ -161,6 +199,33 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
161
199
|
</TouchableOpacity>
|
|
162
200
|
)}
|
|
163
201
|
|
|
202
|
+
{/* Admin start sweepstakes button */}
|
|
203
|
+
{isAdmin && isSweepstakes && competition.status === 'scheduled' && (
|
|
204
|
+
<TouchableOpacity
|
|
205
|
+
onPress={async () => {
|
|
206
|
+
setStartingComp(true);
|
|
207
|
+
try {
|
|
208
|
+
await startSweepstakesCompetition(calcutta_competition_id);
|
|
209
|
+
await refresh();
|
|
210
|
+
} catch {}
|
|
211
|
+
setStartingComp(false);
|
|
212
|
+
}}
|
|
213
|
+
disabled={startingComp}
|
|
214
|
+
style={[styles.ctaButton, { backgroundColor: '#8B5CF6', opacity: startingComp ? 0.7 : 1 }]}
|
|
215
|
+
>
|
|
216
|
+
{startingComp ? (
|
|
217
|
+
<ActivityIndicator size="small" color="#FFF" />
|
|
218
|
+
) : (
|
|
219
|
+
<>
|
|
220
|
+
<Ionicons name="play-circle-outline" size={18} color="#FFF" />
|
|
221
|
+
<Text variant="body" bold style={{ color: '#FFF', marginLeft: 8 }}>
|
|
222
|
+
Start Competition{unassignedItemCount > 0 ? ` (${unassignedItemCount} unassigned)` : ''}
|
|
223
|
+
</Text>
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</TouchableOpacity>
|
|
227
|
+
)}
|
|
228
|
+
|
|
164
229
|
{/* Join button */}
|
|
165
230
|
{isJoinable && !hasJoined && onJoin && (
|
|
166
231
|
<TouchableOpacity
|
|
@@ -179,38 +244,65 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
179
244
|
<ActivityIndicator size="small" color="#FFF" />
|
|
180
245
|
) : (
|
|
181
246
|
<Text variant="body" bold style={{ color: '#FFF' }}>
|
|
182
|
-
{
|
|
247
|
+
{isSweepstakes
|
|
248
|
+
? (isFree ? 'Join Free — Get a Random Team!' : `Join — ${formatCurrency(entryFee, competition.market_type)}`)
|
|
249
|
+
: (isFree ? 'Join - Free!' : `Join - ${formatCurrency(entryFee, competition.market_type)}`)}
|
|
183
250
|
</Text>
|
|
184
251
|
)}
|
|
185
252
|
</TouchableOpacity>
|
|
186
253
|
)}
|
|
187
254
|
|
|
188
|
-
{/* Already joined badge + leave */}
|
|
255
|
+
{/* Already joined badge + assigned item (sweepstakes) + leave */}
|
|
189
256
|
{hasJoined && (
|
|
190
|
-
<View variant="transparent" style={{
|
|
191
|
-
<View variant="transparent" style={
|
|
192
|
-
<
|
|
193
|
-
|
|
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
|
+
)}
|
|
194
283
|
</View>
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}}
|
|
205
|
-
disabled={leaving}
|
|
206
|
-
style={[styles.ctaButtonOutline, { borderColor: theme.colors.status.error, paddingHorizontal: 16, opacity: leaving ? 0.7 : 1 }]}
|
|
207
|
-
>
|
|
208
|
-
{leaving ? (
|
|
209
|
-
<ActivityIndicator size="small" color={theme.colors.status.error} />
|
|
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
|
+
/>
|
|
210
293
|
) : (
|
|
211
|
-
<
|
|
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>
|
|
212
297
|
)}
|
|
213
|
-
|
|
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>
|
|
305
|
+
</View>
|
|
214
306
|
)}
|
|
215
307
|
</View>
|
|
216
308
|
)}
|
|
@@ -226,6 +318,32 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
226
318
|
);
|
|
227
319
|
|
|
228
320
|
case 'payouts':
|
|
321
|
+
// Sweepstakes: show prize list per round instead of payout percentages
|
|
322
|
+
if (isSweepstakes) {
|
|
323
|
+
const prizeRounds = rounds.filter(r => r.prize_description);
|
|
324
|
+
if (prizeRounds.length === 0) return null;
|
|
325
|
+
return (
|
|
326
|
+
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
|
|
327
|
+
<Text variant="body" bold style={styles.sectionTitle}>Prizes</Text>
|
|
328
|
+
{prizeRounds.map((round, i) => (
|
|
329
|
+
<View key={round.calcutta_round_id} variant="transparent" style={[styles.payoutRow, i < prizeRounds.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
330
|
+
{round.prize_image?.url ? (
|
|
331
|
+
<Image
|
|
332
|
+
source={{ uri: round.prize_image.url }}
|
|
333
|
+
style={{ width: 40, height: 40, borderRadius: 6, marginRight: 10 }}
|
|
334
|
+
resizeMode="cover"
|
|
335
|
+
/>
|
|
336
|
+
) : null}
|
|
337
|
+
<View variant="transparent" style={{ flex: 1 }}>
|
|
338
|
+
<Text variant="body" bold>{round.round_name}</Text>
|
|
339
|
+
<Text variant="caption" color="secondary" style={{ marginTop: 2 }}>{round.prize_description}</Text>
|
|
340
|
+
</View>
|
|
341
|
+
</View>
|
|
342
|
+
))}
|
|
343
|
+
</View>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
229
347
|
if (payout_rules.length === 0) return null;
|
|
230
348
|
return (
|
|
231
349
|
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
|
|
@@ -255,12 +373,69 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
255
373
|
if (items.length === 0) return null;
|
|
256
374
|
return (
|
|
257
375
|
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle, paddingBottom: 0 }]}>
|
|
258
|
-
<Text variant="body" bold style={styles.sectionTitle}>Auction Items ({items.length})</Text>
|
|
376
|
+
<Text variant="body" bold style={styles.sectionTitle}>{isSweepstakes ? 'Teams' : 'Auction Items'} ({items.length})</Text>
|
|
259
377
|
</View>
|
|
260
378
|
);
|
|
261
379
|
|
|
262
380
|
case 'items':
|
|
263
381
|
if (items.length === 0) return null;
|
|
382
|
+
if (isSweepstakes) {
|
|
383
|
+
return (
|
|
384
|
+
<View variant="transparent" style={{ paddingHorizontal: 16, paddingBottom: 8 }}>
|
|
385
|
+
<TextInput
|
|
386
|
+
style={{
|
|
387
|
+
height: 36,
|
|
388
|
+
borderRadius: 8,
|
|
389
|
+
borderWidth: 1,
|
|
390
|
+
borderColor: theme.colors.border.subtle,
|
|
391
|
+
backgroundColor: theme.colors.surface.input,
|
|
392
|
+
color: theme.colors.text.primary,
|
|
393
|
+
paddingHorizontal: 12,
|
|
394
|
+
fontSize: 14,
|
|
395
|
+
marginBottom: 8,
|
|
396
|
+
}}
|
|
397
|
+
placeholder="Search by team or owner..."
|
|
398
|
+
placeholderTextColor={theme.colors.text.tertiary}
|
|
399
|
+
value={itemSearch}
|
|
400
|
+
onChangeText={setItemSearch}
|
|
401
|
+
autoCapitalize="none"
|
|
402
|
+
autoCorrect={false}
|
|
403
|
+
onFocus={(e: any) => {
|
|
404
|
+
if (Platform.OS === 'web' && e?.target?.scrollIntoView) {
|
|
405
|
+
setTimeout(() => e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
|
|
406
|
+
}
|
|
407
|
+
}}
|
|
408
|
+
/>
|
|
409
|
+
{filteredItems.map(item => {
|
|
410
|
+
const imgUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
|
|
411
|
+
const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
|
|
412
|
+
const ownerName = owner?.username || owner?.show_name;
|
|
413
|
+
const isMe = item.winning_player_id == player_id;
|
|
414
|
+
return (
|
|
415
|
+
<View key={item.calcutta_auction_item_id} variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle }}>
|
|
416
|
+
{imgUrl ? (
|
|
417
|
+
<Image source={{ uri: imgUrl }} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="cover" />
|
|
418
|
+
) : (
|
|
419
|
+
<View variant="transparent" style={{ width: 28, height: 28, borderRadius: 6, backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }}>
|
|
420
|
+
<Ionicons name="trophy-outline" size={14} color={theme.colors.text.tertiary} />
|
|
421
|
+
</View>
|
|
422
|
+
)}
|
|
423
|
+
<View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
|
|
424
|
+
<Text variant="body">{item.item_name}</Text>
|
|
425
|
+
{ownerName ? (
|
|
426
|
+
<Text variant="caption" color="tertiary">{isMe ? 'You' : ownerName}</Text>
|
|
427
|
+
) : (
|
|
428
|
+
<Text variant="caption" color="tertiary">Available</Text>
|
|
429
|
+
)}
|
|
430
|
+
</View>
|
|
431
|
+
{item.seed != null && <Text variant="caption" color="tertiary">#{item.seed}</Text>}
|
|
432
|
+
{isMe && <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 8 }}>YOU</Text>}
|
|
433
|
+
</View>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
436
|
+
</View>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
264
439
|
return (
|
|
265
440
|
<View variant="transparent" style={{ paddingHorizontal: 16 }}>
|
|
266
441
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
@@ -332,20 +507,25 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
332
507
|
};
|
|
333
508
|
|
|
334
509
|
return (
|
|
335
|
-
<View variant="
|
|
336
|
-
{/* Header bar */}
|
|
337
|
-
|
|
338
|
-
<
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
510
|
+
<View variant="base" style={styles.container}>
|
|
511
|
+
{/* Header bar — hidden for sweepstakes (title is in the hero) */}
|
|
512
|
+
{!isSweepstakes && (
|
|
513
|
+
<View variant="transparent" style={[styles.headerBar, { borderColor: theme.colors.border.subtle }]}>
|
|
514
|
+
<TouchableOpacity onPress={onClose} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
|
515
|
+
<Ionicons name="arrow-back" size={24} color={theme.colors.text.primary} />
|
|
516
|
+
</TouchableOpacity>
|
|
517
|
+
<Text variant="body" bold style={{ flex: 1, marginLeft: 12 }} numberOfLines={1}>{competition.competition_name}</Text>
|
|
518
|
+
</View>
|
|
519
|
+
)}
|
|
343
520
|
|
|
344
521
|
<FlatList
|
|
345
522
|
data={sections}
|
|
346
523
|
keyExtractor={(item) => item.key}
|
|
347
524
|
renderItem={renderSection}
|
|
348
525
|
showsVerticalScrollIndicator={false}
|
|
526
|
+
style={{ backgroundColor: theme.colors.surface.base }}
|
|
527
|
+
contentContainerStyle={{ paddingBottom: Platform.OS === 'web' ? 300 : 40 }}
|
|
528
|
+
keyboardShouldPersistTaps="handled"
|
|
349
529
|
/>
|
|
350
530
|
</View>
|
|
351
531
|
);
|
|
@@ -374,4 +554,6 @@ const styles = StyleSheet.create({
|
|
|
374
554
|
participantRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
|
|
375
555
|
avatarCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 10 },
|
|
376
556
|
youBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8 },
|
|
557
|
+
assignedItemCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 10, borderWidth: 1 },
|
|
558
|
+
assignedItemImage: { width: 44, height: 44, borderRadius: 8 },
|
|
377
559
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { StyleSheet, FlatList } from 'react-native';
|
|
2
|
+
import { StyleSheet, FlatList, Image } from 'react-native';
|
|
3
3
|
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
4
|
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
-
import type { CalcuttaPayoutRuleProps } from '@bettoredge/types';
|
|
5
|
+
import type { CalcuttaPayoutRuleProps, CalcuttaRoundProps } from '@bettoredge/types';
|
|
6
6
|
import { formatCurrency } from '../helpers/formatting';
|
|
7
7
|
|
|
8
8
|
export interface CalcuttaPayoutPreviewProps {
|
|
@@ -11,6 +11,8 @@ export interface CalcuttaPayoutPreviewProps {
|
|
|
11
11
|
market_type: string;
|
|
12
12
|
unclaimed_pot?: number;
|
|
13
13
|
min_spend_pct?: number;
|
|
14
|
+
auction_type?: string;
|
|
15
|
+
rounds?: CalcuttaRoundProps[];
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export const CalcuttaPayoutPreview: React.FC<CalcuttaPayoutPreviewProps> = ({
|
|
@@ -19,8 +21,58 @@ export const CalcuttaPayoutPreview: React.FC<CalcuttaPayoutPreviewProps> = ({
|
|
|
19
21
|
market_type,
|
|
20
22
|
unclaimed_pot,
|
|
21
23
|
min_spend_pct,
|
|
24
|
+
auction_type,
|
|
25
|
+
rounds,
|
|
22
26
|
}) => {
|
|
23
27
|
const { theme } = useTheme();
|
|
28
|
+
const isSweepstakes = auction_type === 'sweepstakes';
|
|
29
|
+
|
|
30
|
+
// Sweepstakes: render prize list per round instead of percentages
|
|
31
|
+
if (isSweepstakes && rounds) {
|
|
32
|
+
const prizeRounds = rounds.filter(r => r.prize_description);
|
|
33
|
+
if (prizeRounds.length === 0) {
|
|
34
|
+
return (
|
|
35
|
+
<View variant="transparent" style={styles.container}>
|
|
36
|
+
<Text variant="caption" color="tertiary" style={styles.emptyText}>
|
|
37
|
+
No prizes defined
|
|
38
|
+
</Text>
|
|
39
|
+
</View>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
<View variant="transparent" style={styles.container}>
|
|
44
|
+
<View
|
|
45
|
+
variant="transparent"
|
|
46
|
+
style={[styles.potSummary, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}
|
|
47
|
+
>
|
|
48
|
+
<Ionicons name="gift-outline" size={20} color={theme.colors.primary.default} />
|
|
49
|
+
<View variant="transparent" style={styles.potInfo}>
|
|
50
|
+
<Text variant="caption" color="tertiary">Prizes</Text>
|
|
51
|
+
<Text variant="body" bold>{prizeRounds.length} round{prizeRounds.length !== 1 ? 's' : ''}</Text>
|
|
52
|
+
</View>
|
|
53
|
+
</View>
|
|
54
|
+
{prizeRounds.map(round => (
|
|
55
|
+
<View
|
|
56
|
+
key={round.calcutta_round_id}
|
|
57
|
+
variant="transparent"
|
|
58
|
+
style={[styles.ruleRow, { borderColor: theme.colors.border.subtle }]}
|
|
59
|
+
>
|
|
60
|
+
{round.prize_image?.url && (
|
|
61
|
+
<Image
|
|
62
|
+
source={{ uri: round.prize_image.url }}
|
|
63
|
+
style={{ width: 36, height: 36, borderRadius: 6, marginRight: 10 }}
|
|
64
|
+
resizeMode="cover"
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
<View variant="transparent" style={styles.ruleInfo}>
|
|
68
|
+
<Text variant="body" numberOfLines={1}>{round.round_name}</Text>
|
|
69
|
+
<Text variant="caption" color="secondary">{round.prize_description}</Text>
|
|
70
|
+
</View>
|
|
71
|
+
</View>
|
|
72
|
+
))}
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
24
76
|
|
|
25
77
|
const roundRules = payout_rules
|
|
26
78
|
.filter(r => r.payout_type === 'round')
|
|
@@ -19,6 +19,7 @@ export interface CalcuttaRoundResultsProps {
|
|
|
19
19
|
market_type: string;
|
|
20
20
|
total_pot?: number;
|
|
21
21
|
unclaimed_pot?: number;
|
|
22
|
+
auction_type?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const CalcuttaRoundResults: React.FC<CalcuttaRoundResultsProps> = ({
|
|
@@ -29,7 +30,9 @@ export const CalcuttaRoundResults: React.FC<CalcuttaRoundResultsProps> = ({
|
|
|
29
30
|
market_type,
|
|
30
31
|
total_pot,
|
|
31
32
|
unclaimed_pot,
|
|
33
|
+
auction_type,
|
|
32
34
|
}) => {
|
|
35
|
+
const isSweepstakes = auction_type === 'sweepstakes';
|
|
33
36
|
const { theme } = useTheme();
|
|
34
37
|
const { roundSummaries } = useCalcuttaResults(rounds, items, item_results, payout_rules);
|
|
35
38
|
const [expandedRound, setExpandedRound] = useState<string | null>(null);
|
|
@@ -72,11 +75,16 @@ export const CalcuttaRoundResults: React.FC<CalcuttaRoundResultsProps> = ({
|
|
|
72
75
|
<Text variant="caption" color="tertiary">
|
|
73
76
|
{getStatusLabel(summary.round.status)}
|
|
74
77
|
</Text>
|
|
75
|
-
{summary.total_payout > 0 && (
|
|
78
|
+
{!isSweepstakes && summary.total_payout > 0 && (
|
|
76
79
|
<Text variant="caption" style={{ color: theme.colors.status.success, marginLeft: 8 }}>
|
|
77
80
|
{formatCurrency(summary.total_payout, market_type)} paid
|
|
78
81
|
</Text>
|
|
79
82
|
)}
|
|
83
|
+
{isSweepstakes && summary.round.prize_description && (
|
|
84
|
+
<Text variant="caption" style={{ color: theme.colors.primary.default, marginLeft: 8 }} numberOfLines={1}>
|
|
85
|
+
Prize available
|
|
86
|
+
</Text>
|
|
87
|
+
)}
|
|
80
88
|
</View>
|
|
81
89
|
</View>
|
|
82
90
|
<View variant="transparent" style={styles.roundCounts}>
|
|
@@ -95,8 +103,18 @@ export const CalcuttaRoundResults: React.FC<CalcuttaRoundResultsProps> = ({
|
|
|
95
103
|
|
|
96
104
|
{isExpanded && (
|
|
97
105
|
<View variant="transparent" style={[styles.roundBody, { borderColor: theme.colors.border.subtle }]}>
|
|
98
|
-
{/*
|
|
99
|
-
{summary.
|
|
106
|
+
{/* Sweepstakes: show prize instead of payout rule */}
|
|
107
|
+
{isSweepstakes && summary.round.prize_description && (
|
|
108
|
+
<View variant="transparent" style={styles.payoutRow}>
|
|
109
|
+
<Ionicons name="gift-outline" size={14} color={theme.colors.primary.default} />
|
|
110
|
+
<Text variant="caption" style={{ marginLeft: 6, color: theme.colors.primary.default, flex: 1 }}>
|
|
111
|
+
Prize: {summary.round.prize_description}
|
|
112
|
+
</Text>
|
|
113
|
+
</View>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Payout rule (non-sweepstakes) */}
|
|
117
|
+
{!isSweepstakes && summary.payout_rule && (
|
|
100
118
|
<View variant="transparent" style={styles.payoutRow}>
|
|
101
119
|
<Ionicons name="cash-outline" size={14} color={theme.colors.text.tertiary} />
|
|
102
120
|
<Text variant="caption" color="tertiary" style={{ marginLeft: 6 }}>
|
|
@@ -105,8 +123,8 @@ export const CalcuttaRoundResults: React.FC<CalcuttaRoundResultsProps> = ({
|
|
|
105
123
|
</View>
|
|
106
124
|
)}
|
|
107
125
|
|
|
108
|
-
{/* Rollover notice for unsold advancing items */}
|
|
109
|
-
{summary.round.status === 'closed' && summary.advanced_items.some(i => !i.winning_player_id) && (
|
|
126
|
+
{/* Rollover notice for unsold advancing items (non-sweepstakes) */}
|
|
127
|
+
{!isSweepstakes && summary.round.status === 'closed' && summary.advanced_items.some(i => !i.winning_player_id) && (
|
|
110
128
|
<View variant="transparent" style={[styles.payoutRow, { marginTop: 2 }]}>
|
|
111
129
|
<Ionicons name="arrow-forward-circle-outline" size={14} color={theme.colors.primary.default} />
|
|
112
130
|
<Text variant="caption" style={{ marginLeft: 6, color: theme.colors.primary.default, flex: 1 }}>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { StyleSheet, Modal, TouchableOpacity, Image, Animated } 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 { resolveItemImageUrl } from '../helpers/formatting';
|
|
7
|
+
|
|
8
|
+
export interface SweepstakesRevealProps {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
item?: CalcuttaAuctionItemProps;
|
|
11
|
+
/** Resolved image URL from sr_svc (getBulkTeams/getBulkAthletes) */
|
|
12
|
+
itemImageUrl?: string;
|
|
13
|
+
competitionName?: string;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SweepstakesReveal: React.FC<SweepstakesRevealProps> = ({
|
|
18
|
+
visible,
|
|
19
|
+
item,
|
|
20
|
+
itemImageUrl,
|
|
21
|
+
competitionName,
|
|
22
|
+
onClose,
|
|
23
|
+
}) => {
|
|
24
|
+
const { theme } = useTheme();
|
|
25
|
+
const scaleAnim = useRef(new Animated.Value(0.3)).current;
|
|
26
|
+
const opacityAnim = useRef(new Animated.Value(0)).current;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (visible && item) {
|
|
30
|
+
scaleAnim.setValue(0.3);
|
|
31
|
+
opacityAnim.setValue(0);
|
|
32
|
+
Animated.parallel([
|
|
33
|
+
Animated.spring(scaleAnim, {
|
|
34
|
+
toValue: 1,
|
|
35
|
+
tension: 50,
|
|
36
|
+
friction: 7,
|
|
37
|
+
useNativeDriver: true,
|
|
38
|
+
}),
|
|
39
|
+
Animated.timing(opacityAnim, {
|
|
40
|
+
toValue: 1,
|
|
41
|
+
duration: 300,
|
|
42
|
+
useNativeDriver: true,
|
|
43
|
+
}),
|
|
44
|
+
]).start();
|
|
45
|
+
}
|
|
46
|
+
}, [visible, item]);
|
|
47
|
+
|
|
48
|
+
if (!item) return null;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Modal
|
|
52
|
+
visible={visible}
|
|
53
|
+
transparent
|
|
54
|
+
animationType="fade"
|
|
55
|
+
onRequestClose={onClose}
|
|
56
|
+
>
|
|
57
|
+
<View variant="transparent" style={styles.backdrop}>
|
|
58
|
+
<Animated.View style={[
|
|
59
|
+
styles.card,
|
|
60
|
+
{
|
|
61
|
+
backgroundColor: theme.colors.surface.elevated,
|
|
62
|
+
borderColor: theme.colors.border.subtle,
|
|
63
|
+
transform: [{ scale: scaleAnim }],
|
|
64
|
+
opacity: opacityAnim,
|
|
65
|
+
},
|
|
66
|
+
]}>
|
|
67
|
+
{/* Confetti-style header */}
|
|
68
|
+
<View variant="transparent" style={[styles.celebrationHeader, { backgroundColor: '#8B5CF6' }]}>
|
|
69
|
+
<Text style={styles.emoji}>🎉</Text>
|
|
70
|
+
<Text variant="h3" bold style={{ color: '#FFF', textAlign: 'center' }}>
|
|
71
|
+
You Got a Team!
|
|
72
|
+
</Text>
|
|
73
|
+
{competitionName && (
|
|
74
|
+
<Text variant="caption" style={{ color: '#FFFFFFCC', marginTop: 4 }}>
|
|
75
|
+
{competitionName}
|
|
76
|
+
</Text>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
|
|
80
|
+
{/* Item display */}
|
|
81
|
+
<View variant="transparent" style={styles.itemSection}>
|
|
82
|
+
{(itemImageUrl || resolveItemImageUrl(item.item_image)) ? (
|
|
83
|
+
<Image
|
|
84
|
+
source={{ uri: (itemImageUrl || resolveItemImageUrl(item.item_image))! }}
|
|
85
|
+
style={styles.itemImage}
|
|
86
|
+
resizeMode="cover"
|
|
87
|
+
/>
|
|
88
|
+
) : (
|
|
89
|
+
<View variant="transparent" style={[styles.itemImage, styles.itemImageFallback, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
90
|
+
<Ionicons name="trophy" size={48} color={theme.colors.primary.default} />
|
|
91
|
+
</View>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<Text variant="h2" bold style={{ marginTop: 16, textAlign: 'center' }}>
|
|
95
|
+
{item.item_name}
|
|
96
|
+
</Text>
|
|
97
|
+
|
|
98
|
+
{item.seed != null && (
|
|
99
|
+
<View variant="transparent" style={[styles.seedBadge, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
100
|
+
<Text variant="body" bold style={{ color: theme.colors.primary.default }}>
|
|
101
|
+
Seed #{item.seed}
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<Text variant="body" color="secondary" style={{ marginTop: 12, textAlign: 'center' }}>
|
|
107
|
+
Cheer them on as they compete through each round!
|
|
108
|
+
</Text>
|
|
109
|
+
</View>
|
|
110
|
+
|
|
111
|
+
{/* Close button */}
|
|
112
|
+
<TouchableOpacity
|
|
113
|
+
style={[styles.closeButton, { backgroundColor: '#8B5CF6' }]}
|
|
114
|
+
onPress={onClose}
|
|
115
|
+
activeOpacity={0.8}
|
|
116
|
+
>
|
|
117
|
+
<Text variant="body" bold style={{ color: '#FFF' }}>Let's Go!</Text>
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
</Animated.View>
|
|
120
|
+
</View>
|
|
121
|
+
</Modal>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const styles = StyleSheet.create({
|
|
126
|
+
backdrop: {
|
|
127
|
+
flex: 1,
|
|
128
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
129
|
+
justifyContent: 'center',
|
|
130
|
+
alignItems: 'center',
|
|
131
|
+
padding: 24,
|
|
132
|
+
},
|
|
133
|
+
card: {
|
|
134
|
+
width: '100%',
|
|
135
|
+
maxWidth: 360,
|
|
136
|
+
borderRadius: 16,
|
|
137
|
+
borderWidth: 1,
|
|
138
|
+
},
|
|
139
|
+
celebrationHeader: {
|
|
140
|
+
paddingTop: 32,
|
|
141
|
+
paddingBottom: 24,
|
|
142
|
+
paddingHorizontal: 20,
|
|
143
|
+
alignItems: 'center',
|
|
144
|
+
borderTopLeftRadius: 15,
|
|
145
|
+
borderTopRightRadius: 15,
|
|
146
|
+
},
|
|
147
|
+
emoji: {
|
|
148
|
+
fontSize: 48,
|
|
149
|
+
marginBottom: 8,
|
|
150
|
+
lineHeight: 56,
|
|
151
|
+
},
|
|
152
|
+
itemSection: {
|
|
153
|
+
padding: 24,
|
|
154
|
+
alignItems: 'center',
|
|
155
|
+
},
|
|
156
|
+
itemImage: {
|
|
157
|
+
width: 120,
|
|
158
|
+
height: 120,
|
|
159
|
+
borderRadius: 16,
|
|
160
|
+
},
|
|
161
|
+
itemImageFallback: {
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
justifyContent: 'center',
|
|
164
|
+
},
|
|
165
|
+
seedBadge: {
|
|
166
|
+
paddingHorizontal: 16,
|
|
167
|
+
paddingVertical: 6,
|
|
168
|
+
borderRadius: 20,
|
|
169
|
+
marginTop: 10,
|
|
170
|
+
},
|
|
171
|
+
closeButton: {
|
|
172
|
+
marginHorizontal: 24,
|
|
173
|
+
marginBottom: 24,
|
|
174
|
+
paddingVertical: 14,
|
|
175
|
+
borderRadius: 10,
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
@@ -45,6 +45,15 @@ export const getItemStatusColor = (status: string): string => {
|
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
/** Unwrap possibly double-wrapped image: { url: string } or { url: { url, secure_url, ... } } */
|
|
49
|
+
export const resolveItemImageUrl = (item_image?: { url: any }): string | undefined => {
|
|
50
|
+
if (!item_image) return undefined;
|
|
51
|
+
const raw = item_image.url;
|
|
52
|
+
if (typeof raw === 'string') return raw;
|
|
53
|
+
if (raw && typeof raw === 'object') return raw.secure_url || raw.url;
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
48
57
|
export const getBidStatusColor = (status: string): string => {
|
|
49
58
|
switch (status) {
|
|
50
59
|
case 'active': return '#3B82F6';
|
|
@@ -15,19 +15,25 @@ export type CalcuttaLifecycleState =
|
|
|
15
15
|
/**
|
|
16
16
|
* Derive the lifecycle state from competition.status and competition.auction_status.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Sweepstakes competitions skip the auctioning phase entirely:
|
|
19
|
+
* pending → scheduled → tournament → completed
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
* When the countdown expires but the server hasn't confirmed, the lifecycle
|
|
23
|
-
* stays 'auctioning' and the orchestrator uses a separate `auctionExpired`
|
|
24
|
-
* flag to disable all bid actions. This prevents showing empty results
|
|
25
|
-
* before the server has resolved winners.
|
|
21
|
+
* @param auction_type - Optional. When 'sweepstakes', the auctioning state is never returned.
|
|
26
22
|
*/
|
|
27
23
|
export function deriveLifecycleState(
|
|
28
24
|
status?: string,
|
|
29
25
|
auction_status?: string,
|
|
26
|
+
auction_type?: string,
|
|
30
27
|
): CalcuttaLifecycleState {
|
|
28
|
+
// ── Sweepstakes: no auction phase ──
|
|
29
|
+
if (auction_type === 'sweepstakes') {
|
|
30
|
+
if (!status || status === 'pending') return 'pending';
|
|
31
|
+
if (status === 'scheduled') return 'scheduled';
|
|
32
|
+
if (status === 'closed') return 'completed';
|
|
33
|
+
// auction_closed, inprogress → tournament
|
|
34
|
+
return 'tournament';
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
// ── Hard gate: server confirmed auction closed ──
|
|
32
38
|
if (auction_status === 'closed') {
|
|
33
39
|
return status === 'closed' ? 'completed' : 'tournament';
|
|
@@ -59,12 +65,12 @@ export function deriveLifecycleState(
|
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/** True when users can place / update / cancel bids */
|
|
62
|
-
export const canBid = (state: CalcuttaLifecycleState): boolean =>
|
|
63
|
-
state === 'auctioning';
|
|
68
|
+
export const canBid = (state: CalcuttaLifecycleState, auction_type?: string): boolean =>
|
|
69
|
+
auction_type === 'sweepstakes' ? false : state === 'auctioning';
|
|
64
70
|
|
|
65
71
|
/** True when escrow deposit / withdraw controls should be interactive */
|
|
66
|
-
export const canManageEscrow = (state: CalcuttaLifecycleState): boolean =>
|
|
67
|
-
state === 'scheduled' || state === 'auctioning';
|
|
72
|
+
export const canManageEscrow = (state: CalcuttaLifecycleState, auction_type?: string): boolean =>
|
|
73
|
+
auction_type === 'sweepstakes' ? false : (state === 'scheduled' || state === 'auctioning');
|
|
68
74
|
|
|
69
75
|
/** True when round results / leaderboard data is meaningful */
|
|
70
76
|
export const showResults = (state: CalcuttaLifecycleState): boolean =>
|
|
@@ -12,6 +12,10 @@ export interface RoundResultSummary {
|
|
|
12
12
|
eliminated_items: CalcuttaAuctionItemProps[];
|
|
13
13
|
payout_rule?: CalcuttaPayoutRuleProps;
|
|
14
14
|
total_payout: number;
|
|
15
|
+
/** Prize description for this round (sweepstakes only) */
|
|
16
|
+
prize_description?: string;
|
|
17
|
+
/** Prize image for this round (sweepstakes only) */
|
|
18
|
+
prize_image?: { url: string };
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export const useCalcuttaResults = (
|
|
@@ -35,6 +39,8 @@ export const useCalcuttaResults = (
|
|
|
35
39
|
eliminated_items: items.filter(i => eliminatedIds.includes(i.calcutta_auction_item_id)),
|
|
36
40
|
payout_rule: rule,
|
|
37
41
|
total_payout: totalPayout,
|
|
42
|
+
prize_description: round.prize_description,
|
|
43
|
+
prize_image: round.prize_image,
|
|
38
44
|
};
|
|
39
45
|
});
|
|
40
46
|
}, [rounds, items, item_results, payout_rules]);
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,9 @@ export type { CalcuttaRoundResultsProps } from './components/CalcuttaRoundResult
|
|
|
26
26
|
export { CalcuttaItemResults } from './components/CalcuttaItemResults';
|
|
27
27
|
export type { CalcuttaItemResultsProps } from './components/CalcuttaItemResults';
|
|
28
28
|
|
|
29
|
+
export { SweepstakesReveal } from './components/SweepstakesReveal';
|
|
30
|
+
export type { SweepstakesRevealProps } from './components/SweepstakesReveal';
|
|
31
|
+
|
|
29
32
|
export { CalcuttaTemplateSelector } from './components/CalcuttaTemplateSelector';
|
|
30
33
|
export type { CalcuttaTemplateSelectorProps } from './components/CalcuttaTemplateSelector';
|
|
31
34
|
|
|
@@ -64,7 +67,7 @@ export { useCalcuttaItemImages } from './hooks/useCalcuttaItemImages';
|
|
|
64
67
|
export type { ItemImageMap } from './hooks/useCalcuttaItemImages';
|
|
65
68
|
|
|
66
69
|
// Helpers
|
|
67
|
-
export { formatCurrency, formatPlace, getStatusLabel, getItemStatusColor, getBidStatusColor } from './helpers/formatting';
|
|
70
|
+
export { formatCurrency, formatPlace, getStatusLabel, getItemStatusColor, getBidStatusColor, resolveItemImageUrl } from './helpers/formatting';
|
|
68
71
|
export { calculatePayoutPreview, validatePayoutRules } from './helpers/payout';
|
|
69
72
|
export { validateBidAmount, validateEscrowDeposit, validateEscrowWithdraw } from './helpers/validation';
|
|
70
73
|
export { deriveLifecycleState, canBid, canManageEscrow, showResults } from './helpers/lifecycleState';
|
package/src/types.ts
CHANGED
|
@@ -27,5 +27,5 @@ export type {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export type CalcuttaStatus = 'pending' | 'scheduled' | 'auction_open' | 'auction_closed' | 'inprogress' | 'closed';
|
|
30
|
-
export type AuctionType = 'sealed_bid' | 'live';
|
|
30
|
+
export type AuctionType = 'sealed_bid' | 'live' | 'sweepstakes';
|
|
31
31
|
export type PayoutType = 'round' | 'placement' | 'both';
|