@bettoredge/calcutta 0.2.0 → 0.3.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 +4 -4
- package/src/components/CalcuttaAuction.tsx +305 -3
- package/src/components/CalcuttaCard.tsx +13 -4
- package/src/components/CalcuttaDetail.tsx +137 -34
- 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.0",
|
|
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 } 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,303 @@ 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="transparent" style={styles.container}>
|
|
384
|
+
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ gap: 16, paddingBottom: 40 }}>
|
|
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
|
+
/>
|
|
579
|
+
{filteredItems.map((item) => {
|
|
580
|
+
const isMyItem = item.winning_player_id == player_id;
|
|
581
|
+
const isEliminated = item.status === 'eliminated';
|
|
582
|
+
const imageUrl = resolveItemImageUrl(item.item_image) || itemImages[item.item_id]?.url;
|
|
583
|
+
const owner = item.winning_player_id ? enrichedPlayers[item.winning_player_id] : undefined;
|
|
584
|
+
const ownerName = owner?.username || owner?.show_name;
|
|
585
|
+
return (
|
|
586
|
+
<View
|
|
587
|
+
key={item.calcutta_auction_item_id}
|
|
588
|
+
variant="transparent"
|
|
589
|
+
style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 8, borderBottomWidth: 1, borderColor: theme.colors.border.subtle, opacity: isEliminated ? 0.5 : 1 }}
|
|
590
|
+
>
|
|
591
|
+
{imageUrl ? (
|
|
592
|
+
<Image source={{ uri: imageUrl }} style={{ width: 28, height: 28, borderRadius: 6 }} resizeMode="cover" />
|
|
593
|
+
) : (
|
|
594
|
+
<Ionicons
|
|
595
|
+
name={isEliminated ? 'close-circle' : isMyItem ? 'star' : 'ellipse-outline'}
|
|
596
|
+
size={16}
|
|
597
|
+
color={isEliminated ? theme.colors.status.error : isMyItem ? theme.colors.primary.default : theme.colors.text.tertiary}
|
|
598
|
+
/>
|
|
599
|
+
)}
|
|
600
|
+
<View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
|
|
601
|
+
<Text variant="body">{item.item_name}</Text>
|
|
602
|
+
{ownerName && (
|
|
603
|
+
<Text variant="caption" color="tertiary">
|
|
604
|
+
{isMyItem ? 'You' : ownerName}
|
|
605
|
+
</Text>
|
|
606
|
+
)}
|
|
607
|
+
{!item.winning_player_id && item.status !== 'unsold' && (
|
|
608
|
+
<Text variant="caption" color="tertiary">Unclaimed</Text>
|
|
609
|
+
)}
|
|
610
|
+
{item.status === 'unsold' && (
|
|
611
|
+
<Text variant="caption" color="tertiary">No owner</Text>
|
|
612
|
+
)}
|
|
613
|
+
</View>
|
|
614
|
+
{item.seed != null && <Text variant="caption" color="tertiary">#{item.seed}</Text>}
|
|
615
|
+
{isMyItem && <Text variant="caption" bold style={{ color: theme.colors.primary.default, marginLeft: 8 }}>YOU</Text>}
|
|
616
|
+
</View>
|
|
617
|
+
);
|
|
618
|
+
})}
|
|
619
|
+
</View>
|
|
620
|
+
)}
|
|
621
|
+
|
|
622
|
+
{!myItem && !isAdmin && (
|
|
623
|
+
<View variant="transparent" style={{ alignItems: 'center', paddingVertical: 20, paddingHorizontal: 16 }}>
|
|
624
|
+
<Ionicons name="time-outline" size={32} color={theme.colors.text.tertiary} />
|
|
625
|
+
<Text variant="body" color="tertiary" style={{ marginTop: 8 }}>
|
|
626
|
+
Waiting for the tournament to begin
|
|
627
|
+
</Text>
|
|
628
|
+
</View>
|
|
629
|
+
)}
|
|
630
|
+
</ScrollView>
|
|
631
|
+
</View>
|
|
632
|
+
);
|
|
633
|
+
};
|
|
634
|
+
|
|
337
635
|
// Main exported component — routes to sealed bid or live layout
|
|
338
636
|
export const CalcuttaAuction: React.FC<CalcuttaAuctionProps> = (props) => {
|
|
339
637
|
const { theme } = useTheme();
|
|
@@ -358,6 +656,10 @@ export const CalcuttaAuction: React.FC<CalcuttaAuctionProps> = (props) => {
|
|
|
358
656
|
);
|
|
359
657
|
}
|
|
360
658
|
|
|
659
|
+
if (competition.auction_type === 'sweepstakes') {
|
|
660
|
+
return <SweepstakesWaiting {...props} competition={competition} />;
|
|
661
|
+
}
|
|
662
|
+
|
|
361
663
|
if (competition.auction_type === 'sealed_bid') {
|
|
362
664
|
return <SealedBidAuction {...props} />;
|
|
363
665
|
}
|
|
@@ -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' : ''}
|
|
@@ -5,7 +5,8 @@ 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 { formatCurrency, getStatusLabel } from '../helpers/formatting';
|
|
8
|
+
import { formatCurrency, getStatusLabel, resolveItemImageUrl } from '../helpers/formatting';
|
|
9
|
+
import { startSweepstakesCompetition } from '@bettoredge/api';
|
|
9
10
|
|
|
10
11
|
export interface CalcuttaDetailProps {
|
|
11
12
|
calcutta_competition_id: string;
|
|
@@ -53,6 +54,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
53
54
|
|
|
54
55
|
const [joining, setJoining] = useState(false);
|
|
55
56
|
const [leaving, setLeaving] = useState(false);
|
|
57
|
+
const [startingComp, setStartingComp] = useState(false);
|
|
56
58
|
|
|
57
59
|
if (loading && !competition) {
|
|
58
60
|
return (
|
|
@@ -75,11 +77,23 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
75
77
|
const hasJoined = participants.some(p => p.player_id == player_id);
|
|
76
78
|
const entryFee = Number(competition.entry_fee) || 0;
|
|
77
79
|
const isFree = entryFee === 0;
|
|
78
|
-
const
|
|
80
|
+
const isSweepstakes = competition.auction_type === 'sweepstakes';
|
|
81
|
+
const isJoinable = isSweepstakes
|
|
82
|
+
? competition.status === 'scheduled'
|
|
83
|
+
: ['scheduled', 'auction_open'].includes(competition.status);
|
|
79
84
|
const totalPot = Number(competition.total_pot) || 0;
|
|
80
|
-
const canLeave = hasJoined && ['pending', 'scheduled'].includes(competition.status);
|
|
85
|
+
const canLeave = hasJoined && !isSweepstakes && ['pending', 'scheduled'].includes(competition.status);
|
|
81
86
|
const heroImage = competition.image?.url;
|
|
82
87
|
|
|
88
|
+
// Sweepstakes: find the player's assigned item
|
|
89
|
+
const myParticipant = participants.find(p => p.player_id == player_id);
|
|
90
|
+
const myAssignedItem = isSweepstakes && hasJoined
|
|
91
|
+
? items.find(i => i.winning_player_id == player_id)
|
|
92
|
+
: undefined;
|
|
93
|
+
const unassignedItemCount = isSweepstakes
|
|
94
|
+
? items.filter(i => !i.winning_player_id && i.status === 'pending').length
|
|
95
|
+
: 0;
|
|
96
|
+
|
|
83
97
|
const sections = [
|
|
84
98
|
{ key: 'hero' },
|
|
85
99
|
{ key: 'info' },
|
|
@@ -112,7 +126,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
112
126
|
<Text variant="caption" bold style={{ color: statusConfig.color }}>{statusConfig.label}</Text>
|
|
113
127
|
</View>
|
|
114
128
|
<Text variant="caption" color="tertiary" style={{ marginLeft: 8 }}>
|
|
115
|
-
{competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
|
|
129
|
+
{isSweepstakes ? 'Sweepstakes' : competition.auction_type === 'live' ? 'Live Auction' : 'Sealed Bid'}
|
|
116
130
|
</Text>
|
|
117
131
|
</View>
|
|
118
132
|
<Text variant="h3" bold>{competition.competition_name}</Text>
|
|
@@ -135,17 +149,24 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
135
149
|
<Text variant="caption" color="tertiary">Entry Fee</Text>
|
|
136
150
|
<Text variant="body" bold>{isFree ? 'FREE' : formatCurrency(entryFee, competition.market_type)}</Text>
|
|
137
151
|
</View>
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
{isSweepstakes ? (
|
|
153
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
154
|
+
<Text variant="caption" color="tertiary">Available</Text>
|
|
155
|
+
<Text variant="body" bold>{unassignedItemCount}/{items.length}</Text>
|
|
156
|
+
</View>
|
|
157
|
+
) : (
|
|
158
|
+
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
159
|
+
<Text variant="caption" color="tertiary">Pot</Text>
|
|
160
|
+
<Text variant="body" bold>{formatCurrency(totalPot, competition.market_type)}</Text>
|
|
161
|
+
</View>
|
|
162
|
+
)}
|
|
142
163
|
<View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
143
164
|
<Text variant="caption" color="tertiary">Players</Text>
|
|
144
165
|
<Text variant="body" bold>{participants.length}{competition.max_participants > 0 ? `/${competition.max_participants}` : ''}</Text>
|
|
145
166
|
</View>
|
|
146
167
|
<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>
|
|
168
|
+
<Text variant="caption" color="tertiary">{isSweepstakes ? 'Prizes' : 'Items'}</Text>
|
|
169
|
+
<Text variant="body" bold>{isSweepstakes ? rounds.filter(r => r.prize_description).length : items.length}</Text>
|
|
149
170
|
</View>
|
|
150
171
|
</View>
|
|
151
172
|
);
|
|
@@ -161,6 +182,33 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
161
182
|
</TouchableOpacity>
|
|
162
183
|
)}
|
|
163
184
|
|
|
185
|
+
{/* Admin start sweepstakes button */}
|
|
186
|
+
{isAdmin && isSweepstakes && competition.status === 'scheduled' && (
|
|
187
|
+
<TouchableOpacity
|
|
188
|
+
onPress={async () => {
|
|
189
|
+
setStartingComp(true);
|
|
190
|
+
try {
|
|
191
|
+
await startSweepstakesCompetition(calcutta_competition_id);
|
|
192
|
+
await refresh();
|
|
193
|
+
} catch {}
|
|
194
|
+
setStartingComp(false);
|
|
195
|
+
}}
|
|
196
|
+
disabled={startingComp}
|
|
197
|
+
style={[styles.ctaButton, { backgroundColor: '#8B5CF6', opacity: startingComp ? 0.7 : 1 }]}
|
|
198
|
+
>
|
|
199
|
+
{startingComp ? (
|
|
200
|
+
<ActivityIndicator size="small" color="#FFF" />
|
|
201
|
+
) : (
|
|
202
|
+
<>
|
|
203
|
+
<Ionicons name="play-circle-outline" size={18} color="#FFF" />
|
|
204
|
+
<Text variant="body" bold style={{ color: '#FFF', marginLeft: 8 }}>
|
|
205
|
+
Start Competition{unassignedItemCount > 0 ? ` (${unassignedItemCount} unassigned)` : ''}
|
|
206
|
+
</Text>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
209
|
+
</TouchableOpacity>
|
|
210
|
+
)}
|
|
211
|
+
|
|
164
212
|
{/* Join button */}
|
|
165
213
|
{isJoinable && !hasJoined && onJoin && (
|
|
166
214
|
<TouchableOpacity
|
|
@@ -179,38 +227,65 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
179
227
|
<ActivityIndicator size="small" color="#FFF" />
|
|
180
228
|
) : (
|
|
181
229
|
<Text variant="body" bold style={{ color: '#FFF' }}>
|
|
182
|
-
{
|
|
230
|
+
{isSweepstakes
|
|
231
|
+
? (isFree ? 'Join Free — Get a Random Team!' : `Join — ${formatCurrency(entryFee, competition.market_type)}`)
|
|
232
|
+
: (isFree ? 'Join - Free!' : `Join - ${formatCurrency(entryFee, competition.market_type)}`)}
|
|
183
233
|
</Text>
|
|
184
234
|
)}
|
|
185
235
|
</TouchableOpacity>
|
|
186
236
|
)}
|
|
187
237
|
|
|
188
|
-
{/* Already joined badge + leave */}
|
|
238
|
+
{/* Already joined badge + assigned item (sweepstakes) + leave */}
|
|
189
239
|
{hasJoined && (
|
|
190
|
-
<View variant="transparent" style={{
|
|
191
|
-
<View variant="transparent" style={
|
|
192
|
-
<
|
|
193
|
-
|
|
240
|
+
<View variant="transparent" style={{ gap: 10 }}>
|
|
241
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
242
|
+
<View variant="transparent" style={[styles.joinedBadge, { flex: 1, backgroundColor: '#10B98115' }]}>
|
|
243
|
+
<Ionicons name="checkmark-circle" size={16} color="#10B981" />
|
|
244
|
+
<Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6 }}>You're in!</Text>
|
|
245
|
+
</View>
|
|
246
|
+
{onLeave && canLeave && (
|
|
247
|
+
<TouchableOpacity
|
|
248
|
+
onPress={async () => {
|
|
249
|
+
setLeaving(true);
|
|
250
|
+
try {
|
|
251
|
+
await onLeave();
|
|
252
|
+
await refresh();
|
|
253
|
+
} catch {}
|
|
254
|
+
setLeaving(false);
|
|
255
|
+
}}
|
|
256
|
+
disabled={leaving}
|
|
257
|
+
style={[styles.ctaButtonOutline, { borderColor: theme.colors.status.error, paddingHorizontal: 16, opacity: leaving ? 0.7 : 1 }]}
|
|
258
|
+
>
|
|
259
|
+
{leaving ? (
|
|
260
|
+
<ActivityIndicator size="small" color={theme.colors.status.error} />
|
|
261
|
+
) : (
|
|
262
|
+
<Text variant="caption" bold style={{ color: theme.colors.status.error }}>Leave</Text>
|
|
263
|
+
)}
|
|
264
|
+
</TouchableOpacity>
|
|
265
|
+
)}
|
|
194
266
|
</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} />
|
|
267
|
+
{/* Sweepstakes: show assigned item */}
|
|
268
|
+
{isSweepstakes && myAssignedItem && (
|
|
269
|
+
<View variant="transparent" style={[styles.assignedItemCard, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
270
|
+
{resolveItemImageUrl(myAssignedItem.item_image) ? (
|
|
271
|
+
<Image
|
|
272
|
+
source={{ uri: resolveItemImageUrl(myAssignedItem.item_image)! }}
|
|
273
|
+
style={styles.assignedItemImage}
|
|
274
|
+
resizeMode="cover"
|
|
275
|
+
/>
|
|
210
276
|
) : (
|
|
211
|
-
<
|
|
277
|
+
<View variant="transparent" style={[styles.assignedItemImage, { backgroundColor: theme.colors.primary.subtle, alignItems: 'center', justifyContent: 'center' }]}>
|
|
278
|
+
<Ionicons name="trophy" size={20} color={theme.colors.primary.default} />
|
|
279
|
+
</View>
|
|
212
280
|
)}
|
|
213
|
-
|
|
281
|
+
<View variant="transparent" style={{ flex: 1, marginLeft: 12 }}>
|
|
282
|
+
<Text variant="caption" color="tertiary">Your Team</Text>
|
|
283
|
+
<Text variant="body" bold>{myAssignedItem.item_name}</Text>
|
|
284
|
+
{myAssignedItem.seed != null && (
|
|
285
|
+
<Text variant="caption" color="tertiary">Seed #{myAssignedItem.seed}</Text>
|
|
286
|
+
)}
|
|
287
|
+
</View>
|
|
288
|
+
</View>
|
|
214
289
|
)}
|
|
215
290
|
</View>
|
|
216
291
|
)}
|
|
@@ -226,6 +301,32 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
226
301
|
);
|
|
227
302
|
|
|
228
303
|
case 'payouts':
|
|
304
|
+
// Sweepstakes: show prize list per round instead of payout percentages
|
|
305
|
+
if (isSweepstakes) {
|
|
306
|
+
const prizeRounds = rounds.filter(r => r.prize_description);
|
|
307
|
+
if (prizeRounds.length === 0) return null;
|
|
308
|
+
return (
|
|
309
|
+
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
|
|
310
|
+
<Text variant="body" bold style={styles.sectionTitle}>Prizes</Text>
|
|
311
|
+
{prizeRounds.map((round, i) => (
|
|
312
|
+
<View key={round.calcutta_round_id} variant="transparent" style={[styles.payoutRow, i < prizeRounds.length - 1 && { borderBottomWidth: 1, borderColor: theme.colors.border.subtle }]}>
|
|
313
|
+
{round.prize_image?.url ? (
|
|
314
|
+
<Image
|
|
315
|
+
source={{ uri: round.prize_image.url }}
|
|
316
|
+
style={{ width: 40, height: 40, borderRadius: 6, marginRight: 10 }}
|
|
317
|
+
resizeMode="cover"
|
|
318
|
+
/>
|
|
319
|
+
) : null}
|
|
320
|
+
<View variant="transparent" style={{ flex: 1 }}>
|
|
321
|
+
<Text variant="body" bold>{round.round_name}</Text>
|
|
322
|
+
<Text variant="caption" color="secondary" style={{ marginTop: 2 }}>{round.prize_description}</Text>
|
|
323
|
+
</View>
|
|
324
|
+
</View>
|
|
325
|
+
))}
|
|
326
|
+
</View>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
229
330
|
if (payout_rules.length === 0) return null;
|
|
230
331
|
return (
|
|
231
332
|
<View variant="transparent" style={[styles.section, { borderColor: theme.colors.border.subtle }]}>
|
|
@@ -255,7 +356,7 @@ export const CalcuttaDetail: React.FC<CalcuttaDetailProps> = ({
|
|
|
255
356
|
if (items.length === 0) return null;
|
|
256
357
|
return (
|
|
257
358
|
<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>
|
|
359
|
+
<Text variant="body" bold style={styles.sectionTitle}>{isSweepstakes ? 'Teams' : 'Auction Items'} ({items.length})</Text>
|
|
259
360
|
</View>
|
|
260
361
|
);
|
|
261
362
|
|
|
@@ -374,4 +475,6 @@ const styles = StyleSheet.create({
|
|
|
374
475
|
participantRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10 },
|
|
375
476
|
avatarCircle: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', marginRight: 10 },
|
|
376
477
|
youBadge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 8 },
|
|
478
|
+
assignedItemCard: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 10, borderWidth: 1 },
|
|
479
|
+
assignedItemImage: { width: 44, height: 44, borderRadius: 8 },
|
|
377
480
|
});
|
|
@@ -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';
|