@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettoredge/calcutta",
3
- "version": "0.2.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.0"
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.0",
44
- "@bettoredge/types": ">=0.5.0"
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 { formatCurrency, getStatusLabel } from '../helpers/formatting';
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 === 'live' ? 'Live Auction' : 'Sealed Bid';
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
- <Text variant="caption" color="secondary">
61
- Pot: {formatCurrency(competition.total_pot, competition.market_type)}
62
- </Text>
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 isJoinable = ['scheduled', 'auction_open'].includes(competition.status);
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
- <View variant="transparent" style={[styles.statBox, { borderLeftWidth: 1, borderColor: theme.colors.border.subtle }]}>
139
- <Text variant="caption" color="tertiary">Pot</Text>
140
- <Text variant="body" bold>{formatCurrency(totalPot, competition.market_type)}</Text>
141
- </View>
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
- {isFree ? 'Join - Free!' : `Join - ${formatCurrency(entryFee, competition.market_type)}`}
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={{ flexDirection: 'row', alignItems: 'center', gap: 10 }}>
191
- <View variant="transparent" style={[styles.joinedBadge, { flex: 1, backgroundColor: '#10B98115' }]}>
192
- <Ionicons name="checkmark-circle" size={16} color="#10B981" />
193
- <Text variant="caption" bold style={{ color: '#10B981', marginLeft: 6 }}>You're in!</Text>
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
- {onLeave && canLeave && (
196
- <TouchableOpacity
197
- onPress={async () => {
198
- setLeaving(true);
199
- try {
200
- await onLeave();
201
- await refresh();
202
- } catch {}
203
- setLeaving(false);
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
- <Text variant="caption" bold style={{ color: theme.colors.status.error }}>Leave</Text>
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
- </TouchableOpacity>
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
- {/* Payout rule */}
99
- {summary.payout_rule && (
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
- * CRITICAL: auction_status === 'closed' is the hard gate. If the server says
19
- * the auction is closed we NEVER return 'auctioning', no matter what `status` says.
18
+ * Sweepstakes competitions skip the auctioning phase entirely:
19
+ * pending scheduled tournament completed
20
20
  *
21
- * NOTE: We intentionally do NOT accept a client-side timer override here.
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';