@bettoredge/calcutta 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/components/CalcuttaAuction.tsx +453 -0
- package/src/components/CalcuttaAuctionItem.tsx +292 -0
- package/src/components/CalcuttaBidInput.tsx +214 -0
- package/src/components/CalcuttaCard.tsx +131 -0
- package/src/components/CalcuttaDetail.tsx +377 -0
- package/src/components/CalcuttaEscrow.tsx +464 -0
- package/src/components/CalcuttaItemResults.tsx +207 -0
- package/src/components/CalcuttaLeaderboard.tsx +179 -0
- package/src/components/CalcuttaPayoutPreview.tsx +194 -0
- package/src/components/CalcuttaRoundResults.tsx +250 -0
- package/src/components/CalcuttaTemplateSelector.tsx +124 -0
- package/src/components/sealed/AuctionResultsModal.tsx +165 -0
- package/src/components/sealed/EscrowBottomSheet.tsx +185 -0
- package/src/components/sealed/SealedBidAuction.tsx +541 -0
- package/src/components/sealed/SealedBidHeader.tsx +116 -0
- package/src/components/sealed/SealedBidInfoTab.tsx +247 -0
- package/src/components/sealed/SealedBidItemCard.tsx +385 -0
- package/src/components/sealed/SealedBidItemsTab.tsx +235 -0
- package/src/components/sealed/SealedBidMyBidsTab.tsx +512 -0
- package/src/components/sealed/SealedBidPlayersTab.tsx +220 -0
- package/src/components/sealed/SealedBidStatusBar.tsx +415 -0
- package/src/components/sealed/SealedBidTabBar.tsx +172 -0
- package/src/helpers/formatting.ts +56 -0
- package/src/helpers/lifecycleState.ts +71 -0
- package/src/helpers/payout.ts +39 -0
- package/src/helpers/validation.ts +64 -0
- package/src/hooks/useCalcuttaAuction.ts +164 -0
- package/src/hooks/useCalcuttaBid.ts +43 -0
- package/src/hooks/useCalcuttaCompetition.ts +63 -0
- package/src/hooks/useCalcuttaEscrow.ts +52 -0
- package/src/hooks/useCalcuttaItemImages.ts +79 -0
- package/src/hooks/useCalcuttaPlayers.ts +46 -0
- package/src/hooks/useCalcuttaResults.ts +58 -0
- package/src/hooks/useCalcuttaSocket.ts +131 -0
- package/src/hooks/useCalcuttaTemplates.ts +36 -0
- package/src/index.ts +74 -0
- package/src/types.ts +31 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, FlatList, Image } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaParticipantProps, PublicPlayerProps } from '@bettoredge/types';
|
|
6
|
+
import { formatCurrency, formatPlace } from '../helpers/formatting';
|
|
7
|
+
|
|
8
|
+
export interface CalcuttaLeaderboardProps {
|
|
9
|
+
participants: CalcuttaParticipantProps[];
|
|
10
|
+
players?: Record<string, PublicPlayerProps>;
|
|
11
|
+
player_id?: string;
|
|
12
|
+
market_type: string;
|
|
13
|
+
listHeader?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CalcuttaLeaderboard: React.FC<CalcuttaLeaderboardProps> = ({
|
|
17
|
+
participants,
|
|
18
|
+
players,
|
|
19
|
+
player_id,
|
|
20
|
+
market_type,
|
|
21
|
+
listHeader,
|
|
22
|
+
}) => {
|
|
23
|
+
const { theme } = useTheme();
|
|
24
|
+
|
|
25
|
+
const sorted = [...participants].sort((a, b) => {
|
|
26
|
+
if (a.place && b.place) return a.place - b.place;
|
|
27
|
+
if (a.place) return -1;
|
|
28
|
+
if (b.place) return 1;
|
|
29
|
+
return b.total_winnings - a.total_winnings;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const renderHeader = () => (
|
|
33
|
+
<View variant="transparent">
|
|
34
|
+
<>{listHeader}</>
|
|
35
|
+
<View
|
|
36
|
+
variant="transparent"
|
|
37
|
+
style={[styles.headerRow, { borderColor: theme.colors.border.subtle, backgroundColor: theme.colors.surface.elevated }]}
|
|
38
|
+
>
|
|
39
|
+
<Text variant="caption" color="tertiary" style={styles.placeCol}>#</Text>
|
|
40
|
+
<Text variant="caption" color="tertiary" style={styles.playerCol}>Player</Text>
|
|
41
|
+
<Text variant="caption" color="tertiary" style={styles.numCol}>Items</Text>
|
|
42
|
+
<Text variant="caption" color="tertiary" style={styles.numCol}>Spent</Text>
|
|
43
|
+
<Text variant="caption" color="tertiary" style={styles.numCol}>Won</Text>
|
|
44
|
+
</View>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const renderItem = ({ item, index }: { item: CalcuttaParticipantProps; index: number }) => {
|
|
49
|
+
const place = item.place ?? index + 1;
|
|
50
|
+
const isPositive = item.total_winnings > item.total_spent;
|
|
51
|
+
const isMe = item.player_id === player_id;
|
|
52
|
+
const player = players?.[item.player_id];
|
|
53
|
+
const username = player?.username || player?.show_name || `Player ${item.player_id.slice(0, 6)}`;
|
|
54
|
+
const profilePic = player?.profile_pic;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View
|
|
58
|
+
variant="transparent"
|
|
59
|
+
style={[styles.row, { borderColor: theme.colors.border.subtle }, isMe && { backgroundColor: theme.colors.primary.subtle + '40' }]}
|
|
60
|
+
>
|
|
61
|
+
<View variant="transparent" style={styles.placeCol}>
|
|
62
|
+
{place <= 3 ? (
|
|
63
|
+
<Ionicons
|
|
64
|
+
name="trophy"
|
|
65
|
+
size={14}
|
|
66
|
+
color={place === 1 ? '#FFD700' : place === 2 ? '#C0C0C0' : '#CD7F32'}
|
|
67
|
+
/>
|
|
68
|
+
) : (
|
|
69
|
+
<Text variant="caption" color="secondary">{formatPlace(place)}</Text>
|
|
70
|
+
)}
|
|
71
|
+
</View>
|
|
72
|
+
<View variant="transparent" style={styles.playerInfo}>
|
|
73
|
+
{profilePic ? (
|
|
74
|
+
<Image source={{ uri: profilePic }} style={styles.avatar} />
|
|
75
|
+
) : (
|
|
76
|
+
<View variant="transparent" style={[styles.avatar, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
77
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10 }}>
|
|
78
|
+
{username.charAt(0).toUpperCase()}
|
|
79
|
+
</Text>
|
|
80
|
+
</View>
|
|
81
|
+
)}
|
|
82
|
+
<View variant="transparent" style={{ flex: 1, marginLeft: 8 }}>
|
|
83
|
+
<View variant="transparent" style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
84
|
+
<Text variant="body" bold={isMe} numberOfLines={1} style={{ flex: 1 }}>{username}</Text>
|
|
85
|
+
{isMe && (
|
|
86
|
+
<View variant="transparent" style={[styles.youBadge, { backgroundColor: theme.colors.primary.subtle }]}>
|
|
87
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 9 }}>YOU</Text>
|
|
88
|
+
</View>
|
|
89
|
+
)}
|
|
90
|
+
</View>
|
|
91
|
+
</View>
|
|
92
|
+
</View>
|
|
93
|
+
<Text variant="caption" color="secondary" style={styles.numCol}>
|
|
94
|
+
{item.items_owned}
|
|
95
|
+
</Text>
|
|
96
|
+
<Text variant="caption" color="secondary" style={styles.numCol}>
|
|
97
|
+
{formatCurrency(item.total_spent, market_type)}
|
|
98
|
+
</Text>
|
|
99
|
+
<Text
|
|
100
|
+
variant="caption"
|
|
101
|
+
bold={isPositive}
|
|
102
|
+
style={[
|
|
103
|
+
styles.numCol,
|
|
104
|
+
{ color: isPositive ? theme.colors.status.success : theme.colors.text.secondary },
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
{formatCurrency(item.total_winnings, market_type)}
|
|
108
|
+
</Text>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<FlatList
|
|
115
|
+
data={sorted}
|
|
116
|
+
keyExtractor={item => item.calcutta_participant_id}
|
|
117
|
+
ListHeaderComponent={renderHeader}
|
|
118
|
+
renderItem={renderItem}
|
|
119
|
+
ListEmptyComponent={
|
|
120
|
+
<Text variant="caption" color="tertiary" style={styles.emptyText}>
|
|
121
|
+
No participants yet
|
|
122
|
+
</Text>
|
|
123
|
+
}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const styles = StyleSheet.create({
|
|
129
|
+
headerRow: {
|
|
130
|
+
flexDirection: 'row',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
paddingVertical: 8,
|
|
133
|
+
paddingHorizontal: 12,
|
|
134
|
+
borderBottomWidth: 1,
|
|
135
|
+
},
|
|
136
|
+
row: {
|
|
137
|
+
flexDirection: 'row',
|
|
138
|
+
alignItems: 'center',
|
|
139
|
+
paddingVertical: 10,
|
|
140
|
+
paddingHorizontal: 12,
|
|
141
|
+
borderBottomWidth: 1,
|
|
142
|
+
},
|
|
143
|
+
placeCol: {
|
|
144
|
+
width: 32,
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
},
|
|
147
|
+
playerInfo: {
|
|
148
|
+
flex: 1,
|
|
149
|
+
flexDirection: 'row',
|
|
150
|
+
alignItems: 'center',
|
|
151
|
+
marginLeft: 4,
|
|
152
|
+
},
|
|
153
|
+
playerCol: {
|
|
154
|
+
flex: 1,
|
|
155
|
+
marginLeft: 4,
|
|
156
|
+
},
|
|
157
|
+
avatar: {
|
|
158
|
+
width: 28,
|
|
159
|
+
height: 28,
|
|
160
|
+
borderRadius: 14,
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
justifyContent: 'center',
|
|
163
|
+
overflow: 'hidden',
|
|
164
|
+
},
|
|
165
|
+
youBadge: {
|
|
166
|
+
paddingHorizontal: 5,
|
|
167
|
+
paddingVertical: 1,
|
|
168
|
+
borderRadius: 4,
|
|
169
|
+
marginLeft: 4,
|
|
170
|
+
},
|
|
171
|
+
numCol: {
|
|
172
|
+
width: 60,
|
|
173
|
+
textAlign: 'right',
|
|
174
|
+
},
|
|
175
|
+
emptyText: {
|
|
176
|
+
textAlign: 'center',
|
|
177
|
+
padding: 20,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, FlatList } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaPayoutRuleProps } from '@bettoredge/types';
|
|
6
|
+
import { formatCurrency } from '../helpers/formatting';
|
|
7
|
+
|
|
8
|
+
export interface CalcuttaPayoutPreviewProps {
|
|
9
|
+
payout_rules: CalcuttaPayoutRuleProps[];
|
|
10
|
+
total_pot: number;
|
|
11
|
+
market_type: string;
|
|
12
|
+
unclaimed_pot?: number;
|
|
13
|
+
min_spend_pct?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const CalcuttaPayoutPreview: React.FC<CalcuttaPayoutPreviewProps> = ({
|
|
17
|
+
payout_rules,
|
|
18
|
+
total_pot,
|
|
19
|
+
market_type,
|
|
20
|
+
unclaimed_pot,
|
|
21
|
+
min_spend_pct,
|
|
22
|
+
}) => {
|
|
23
|
+
const { theme } = useTheme();
|
|
24
|
+
|
|
25
|
+
const roundRules = payout_rules
|
|
26
|
+
.filter(r => r.payout_type === 'round')
|
|
27
|
+
.sort((a, b) => (a.round_number ?? 0) - (b.round_number ?? 0));
|
|
28
|
+
|
|
29
|
+
const placementRules = payout_rules
|
|
30
|
+
.filter(r => r.payout_type === 'placement')
|
|
31
|
+
.sort((a, b) => (a.placement ?? 0) - (b.placement ?? 0));
|
|
32
|
+
|
|
33
|
+
const totalPct = payout_rules.reduce((sum, r) => sum + r.payout_pct, 0);
|
|
34
|
+
|
|
35
|
+
const renderRule = (rule: CalcuttaPayoutRuleProps) => {
|
|
36
|
+
const amount = (rule.payout_pct / 100) * total_pot;
|
|
37
|
+
return (
|
|
38
|
+
<View
|
|
39
|
+
key={rule.calcutta_payout_rule_id}
|
|
40
|
+
variant="transparent"
|
|
41
|
+
style={[styles.ruleRow, { borderColor: theme.colors.border.subtle }]}
|
|
42
|
+
>
|
|
43
|
+
<View variant="transparent" style={styles.ruleInfo}>
|
|
44
|
+
<Text variant="body" numberOfLines={1}>
|
|
45
|
+
{rule.description ?? `${rule.payout_type === 'round' ? `Round ${rule.round_number}` : `${rule.placement}${getOrdinal(rule.placement ?? 0)} Place`}`}
|
|
46
|
+
</Text>
|
|
47
|
+
<Text variant="caption" color="tertiary">
|
|
48
|
+
{rule.payout_type === 'round' ? 'Round Payout' : 'Placement Payout'}
|
|
49
|
+
</Text>
|
|
50
|
+
</View>
|
|
51
|
+
<View variant="transparent" style={styles.ruleAmounts}>
|
|
52
|
+
<Text variant="body" bold>
|
|
53
|
+
{formatCurrency(amount, market_type)}
|
|
54
|
+
</Text>
|
|
55
|
+
<Text variant="caption" color="tertiary">
|
|
56
|
+
{rule.payout_pct.toFixed(1)}%
|
|
57
|
+
</Text>
|
|
58
|
+
</View>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View variant="transparent" style={styles.container}>
|
|
65
|
+
{/* Total pot summary */}
|
|
66
|
+
<View
|
|
67
|
+
variant="transparent"
|
|
68
|
+
style={[styles.potSummary, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}
|
|
69
|
+
>
|
|
70
|
+
<Ionicons name="cash-outline" size={20} color={theme.colors.primary.default} />
|
|
71
|
+
<View variant="transparent" style={styles.potInfo}>
|
|
72
|
+
<Text variant="caption" color="tertiary">Total Pot</Text>
|
|
73
|
+
<Text variant="body" bold>
|
|
74
|
+
{formatCurrency(total_pot, market_type)}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
<View variant="transparent" style={styles.potInfo}>
|
|
78
|
+
<Text variant="caption" color="tertiary">Allocated</Text>
|
|
79
|
+
<Text variant="body" bold>
|
|
80
|
+
{totalPct.toFixed(1)}%
|
|
81
|
+
</Text>
|
|
82
|
+
</View>
|
|
83
|
+
</View>
|
|
84
|
+
|
|
85
|
+
{/* Min spend rule note */}
|
|
86
|
+
{min_spend_pct != null && Number(min_spend_pct) > 0 && (
|
|
87
|
+
<View
|
|
88
|
+
variant="transparent"
|
|
89
|
+
style={[styles.rolloverBanner, { backgroundColor: theme.colors.primary.subtle, borderColor: theme.colors.border.subtle }]}
|
|
90
|
+
>
|
|
91
|
+
<Ionicons name="alert-circle-outline" size={16} color={theme.colors.primary.default} />
|
|
92
|
+
<View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
|
|
93
|
+
<Text variant="caption" style={{ color: theme.colors.text.secondary }}>
|
|
94
|
+
Min spend rule active: {Number(min_spend_pct)}%. If you deposit $300 but only bid $200, the ${((Number(min_spend_pct) / 100) * 300 - 200).toFixed(0)} shortfall is added to the pot.
|
|
95
|
+
</Text>
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{/* Unclaimed rollover notice */}
|
|
101
|
+
{(unclaimed_pot ?? 0) > 0 && (
|
|
102
|
+
<View
|
|
103
|
+
variant="transparent"
|
|
104
|
+
style={[styles.rolloverBanner, { backgroundColor: theme.colors.primary.subtle, borderColor: theme.colors.border.subtle }]}
|
|
105
|
+
>
|
|
106
|
+
<Ionicons name="swap-horizontal-outline" size={16} color={theme.colors.primary.default} />
|
|
107
|
+
<View variant="transparent" style={{ marginLeft: 8, flex: 1 }}>
|
|
108
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default }}>
|
|
109
|
+
{formatCurrency(unclaimed_pot ?? 0, market_type)} rolling forward
|
|
110
|
+
</Text>
|
|
111
|
+
<Text variant="caption" style={{ color: theme.colors.text.secondary, marginTop: 2 }}>
|
|
112
|
+
From unbid teams that advanced. This amount is added to the next round's pool. After the final round, any remaining amount is split among all players based on how much they spent.
|
|
113
|
+
</Text>
|
|
114
|
+
</View>
|
|
115
|
+
</View>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Round-based payouts */}
|
|
119
|
+
{roundRules.length > 0 && (
|
|
120
|
+
<>
|
|
121
|
+
<View variant="transparent" style={styles.sectionHeader}>
|
|
122
|
+
<Text variant="caption" bold color="secondary">Round Payouts</Text>
|
|
123
|
+
</View>
|
|
124
|
+
{roundRules.map(renderRule)}
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{/* Placement-based payouts */}
|
|
129
|
+
{placementRules.length > 0 && (
|
|
130
|
+
<>
|
|
131
|
+
<View variant="transparent" style={styles.sectionHeader}>
|
|
132
|
+
<Text variant="caption" bold color="secondary">Placement Payouts</Text>
|
|
133
|
+
</View>
|
|
134
|
+
{placementRules.map(renderRule)}
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{payout_rules.length === 0 && (
|
|
139
|
+
<Text variant="caption" color="tertiary" style={styles.emptyText}>
|
|
140
|
+
No payout rules defined
|
|
141
|
+
</Text>
|
|
142
|
+
)}
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const getOrdinal = (n: number): string => {
|
|
148
|
+
const suffixes = ['th', 'st', 'nd', 'rd'];
|
|
149
|
+
const v = n % 100;
|
|
150
|
+
return (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const styles = StyleSheet.create({
|
|
154
|
+
container: {
|
|
155
|
+
flex: 1,
|
|
156
|
+
},
|
|
157
|
+
potSummary: {
|
|
158
|
+
flexDirection: 'row',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
padding: 14,
|
|
161
|
+
borderBottomWidth: 1,
|
|
162
|
+
},
|
|
163
|
+
potInfo: {
|
|
164
|
+
marginLeft: 16,
|
|
165
|
+
},
|
|
166
|
+
sectionHeader: {
|
|
167
|
+
paddingHorizontal: 12,
|
|
168
|
+
paddingVertical: 8,
|
|
169
|
+
},
|
|
170
|
+
ruleRow: {
|
|
171
|
+
flexDirection: 'row',
|
|
172
|
+
alignItems: 'center',
|
|
173
|
+
paddingVertical: 10,
|
|
174
|
+
paddingHorizontal: 12,
|
|
175
|
+
borderBottomWidth: 1,
|
|
176
|
+
},
|
|
177
|
+
ruleInfo: {
|
|
178
|
+
flex: 1,
|
|
179
|
+
},
|
|
180
|
+
ruleAmounts: {
|
|
181
|
+
alignItems: 'flex-end',
|
|
182
|
+
marginLeft: 10,
|
|
183
|
+
},
|
|
184
|
+
emptyText: {
|
|
185
|
+
textAlign: 'center',
|
|
186
|
+
padding: 20,
|
|
187
|
+
},
|
|
188
|
+
rolloverBanner: {
|
|
189
|
+
flexDirection: 'row',
|
|
190
|
+
alignItems: 'flex-start',
|
|
191
|
+
padding: 12,
|
|
192
|
+
borderBottomWidth: 1,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type {
|
|
6
|
+
CalcuttaRoundProps,
|
|
7
|
+
CalcuttaAuctionItemProps,
|
|
8
|
+
CalcuttaItemResultProps,
|
|
9
|
+
CalcuttaPayoutRuleProps,
|
|
10
|
+
} from '@bettoredge/types';
|
|
11
|
+
import { useCalcuttaResults, RoundResultSummary } from '../hooks/useCalcuttaResults';
|
|
12
|
+
import { formatCurrency, getStatusLabel } from '../helpers/formatting';
|
|
13
|
+
|
|
14
|
+
export interface CalcuttaRoundResultsProps {
|
|
15
|
+
rounds: CalcuttaRoundProps[];
|
|
16
|
+
items: CalcuttaAuctionItemProps[];
|
|
17
|
+
item_results: CalcuttaItemResultProps[];
|
|
18
|
+
payout_rules: CalcuttaPayoutRuleProps[];
|
|
19
|
+
market_type: string;
|
|
20
|
+
total_pot?: number;
|
|
21
|
+
unclaimed_pot?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const CalcuttaRoundResults: React.FC<CalcuttaRoundResultsProps> = ({
|
|
25
|
+
rounds,
|
|
26
|
+
items,
|
|
27
|
+
item_results,
|
|
28
|
+
payout_rules,
|
|
29
|
+
market_type,
|
|
30
|
+
total_pot,
|
|
31
|
+
unclaimed_pot,
|
|
32
|
+
}) => {
|
|
33
|
+
const { theme } = useTheme();
|
|
34
|
+
const { roundSummaries } = useCalcuttaResults(rounds, items, item_results, payout_rules);
|
|
35
|
+
const [expandedRound, setExpandedRound] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
if (rounds.length === 0) {
|
|
38
|
+
return (
|
|
39
|
+
<View variant="transparent" style={styles.emptyContainer}>
|
|
40
|
+
<Text variant="caption" color="tertiary">No rounds yet</Text>
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const toggleRound = (roundId: string) => {
|
|
46
|
+
setExpandedRound(prev => prev === roundId ? null : roundId);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const renderRound = (summary: RoundResultSummary) => {
|
|
50
|
+
const isExpanded = expandedRound === summary.round.calcutta_round_id;
|
|
51
|
+
const statusColor = summary.round.status === 'closed'
|
|
52
|
+
? theme.colors.status.success
|
|
53
|
+
: summary.round.status === 'inprogress'
|
|
54
|
+
? theme.colors.primary.default
|
|
55
|
+
: theme.colors.text.tertiary;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<View
|
|
59
|
+
key={summary.round.calcutta_round_id}
|
|
60
|
+
variant="transparent"
|
|
61
|
+
style={[styles.roundCard, { borderColor: theme.colors.border.subtle }]}
|
|
62
|
+
>
|
|
63
|
+
<TouchableOpacity
|
|
64
|
+
style={styles.roundHeader}
|
|
65
|
+
onPress={() => toggleRound(summary.round.calcutta_round_id)}
|
|
66
|
+
activeOpacity={0.7}
|
|
67
|
+
>
|
|
68
|
+
<View variant="transparent" style={styles.roundInfo}>
|
|
69
|
+
<Text variant="body" bold>{summary.round.round_name}</Text>
|
|
70
|
+
<View variant="transparent" style={styles.roundMeta}>
|
|
71
|
+
<View variant="transparent" style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
|
72
|
+
<Text variant="caption" color="tertiary">
|
|
73
|
+
{getStatusLabel(summary.round.status)}
|
|
74
|
+
</Text>
|
|
75
|
+
{summary.total_payout > 0 && (
|
|
76
|
+
<Text variant="caption" style={{ color: theme.colors.status.success, marginLeft: 8 }}>
|
|
77
|
+
{formatCurrency(summary.total_payout, market_type)} paid
|
|
78
|
+
</Text>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
</View>
|
|
82
|
+
<View variant="transparent" style={styles.roundCounts}>
|
|
83
|
+
<Text variant="caption" bold style={{ color: theme.colors.status.success }}>
|
|
84
|
+
{summary.advanced_items.length}/{summary.advanced_items.length + summary.eliminated_items.length}
|
|
85
|
+
</Text>
|
|
86
|
+
<Text variant="caption" color="tertiary" style={{ marginLeft: 4 }}>advanced</Text>
|
|
87
|
+
</View>
|
|
88
|
+
<Ionicons
|
|
89
|
+
name={isExpanded ? 'chevron-up' : 'chevron-down'}
|
|
90
|
+
size={16}
|
|
91
|
+
color={theme.colors.text.tertiary}
|
|
92
|
+
style={styles.chevron}
|
|
93
|
+
/>
|
|
94
|
+
</TouchableOpacity>
|
|
95
|
+
|
|
96
|
+
{isExpanded && (
|
|
97
|
+
<View variant="transparent" style={[styles.roundBody, { borderColor: theme.colors.border.subtle }]}>
|
|
98
|
+
{/* Payout rule */}
|
|
99
|
+
{summary.payout_rule && (
|
|
100
|
+
<View variant="transparent" style={styles.payoutRow}>
|
|
101
|
+
<Ionicons name="cash-outline" size={14} color={theme.colors.text.tertiary} />
|
|
102
|
+
<Text variant="caption" color="tertiary" style={{ marginLeft: 6 }}>
|
|
103
|
+
{summary.payout_rule.payout_pct}% of pot split among advancing teams
|
|
104
|
+
</Text>
|
|
105
|
+
</View>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Rollover notice for unsold advancing items */}
|
|
109
|
+
{summary.round.status === 'closed' && summary.advanced_items.some(i => !i.winning_player_id) && (
|
|
110
|
+
<View variant="transparent" style={[styles.payoutRow, { marginTop: 2 }]}>
|
|
111
|
+
<Ionicons name="arrow-forward-circle-outline" size={14} color={theme.colors.primary.default} />
|
|
112
|
+
<Text variant="caption" style={{ marginLeft: 6, color: theme.colors.primary.default, flex: 1 }}>
|
|
113
|
+
Unbid teams' share rolls into the next round. Any amount remaining after the final round is redistributed to all players proportionally.
|
|
114
|
+
</Text>
|
|
115
|
+
</View>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Advanced items */}
|
|
119
|
+
{summary.advanced_items.length > 0 && (
|
|
120
|
+
<>
|
|
121
|
+
<Text variant="caption" bold color="secondary" style={styles.sectionLabel}>
|
|
122
|
+
Advanced ({summary.advanced_items.length})
|
|
123
|
+
</Text>
|
|
124
|
+
{summary.advanced_items.map(item => {
|
|
125
|
+
const isUnsold = !item.winning_player_id;
|
|
126
|
+
return (
|
|
127
|
+
<View key={item.calcutta_auction_item_id} variant="transparent" style={styles.itemRow}>
|
|
128
|
+
<Ionicons
|
|
129
|
+
name={isUnsold ? 'help-circle' : 'checkmark-circle'}
|
|
130
|
+
size={14}
|
|
131
|
+
color={isUnsold ? theme.colors.text.tertiary : theme.colors.status.success}
|
|
132
|
+
/>
|
|
133
|
+
<Text variant="caption" style={[styles.itemName, isUnsold && { color: theme.colors.text.tertiary }]} numberOfLines={1}>
|
|
134
|
+
{item.item_name}{isUnsold ? ' (unbid)' : ''}
|
|
135
|
+
</Text>
|
|
136
|
+
{item.seed != null && (
|
|
137
|
+
<Text variant="caption" color="tertiary">#{item.seed}</Text>
|
|
138
|
+
)}
|
|
139
|
+
</View>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
{/* Eliminated items */}
|
|
146
|
+
{summary.eliminated_items.length > 0 && (
|
|
147
|
+
<>
|
|
148
|
+
<Text variant="caption" bold color="secondary" style={styles.sectionLabel}>
|
|
149
|
+
Eliminated ({summary.eliminated_items.length})
|
|
150
|
+
</Text>
|
|
151
|
+
{summary.eliminated_items.map(item => {
|
|
152
|
+
const isUnsold = !item.winning_player_id;
|
|
153
|
+
return (
|
|
154
|
+
<View key={item.calcutta_auction_item_id} variant="transparent" style={styles.itemRow}>
|
|
155
|
+
<Ionicons name="close-circle" size={14} color={theme.colors.status.error} />
|
|
156
|
+
<Text variant="caption" style={[styles.itemName, isUnsold && { color: theme.colors.text.tertiary }]} numberOfLines={1}>
|
|
157
|
+
{item.item_name}{isUnsold ? ' (unbid)' : ''}
|
|
158
|
+
</Text>
|
|
159
|
+
{item.seed != null && (
|
|
160
|
+
<Text variant="caption" color="tertiary">#{item.seed}</Text>
|
|
161
|
+
)}
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{summary.advanced_items.length === 0 && summary.eliminated_items.length === 0 && (
|
|
169
|
+
<Text variant="caption" color="tertiary" style={styles.noResults}>
|
|
170
|
+
No results yet for this round
|
|
171
|
+
</Text>
|
|
172
|
+
)}
|
|
173
|
+
</View>
|
|
174
|
+
)}
|
|
175
|
+
</View>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<ScrollView style={styles.container}>
|
|
181
|
+
{roundSummaries.map(renderRound)}
|
|
182
|
+
</ScrollView>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const styles = StyleSheet.create({
|
|
187
|
+
container: {
|
|
188
|
+
flex: 1,
|
|
189
|
+
},
|
|
190
|
+
emptyContainer: {
|
|
191
|
+
alignItems: 'center',
|
|
192
|
+
padding: 20,
|
|
193
|
+
},
|
|
194
|
+
roundCard: {
|
|
195
|
+
borderBottomWidth: 1,
|
|
196
|
+
},
|
|
197
|
+
roundHeader: {
|
|
198
|
+
flexDirection: 'row',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
padding: 12,
|
|
201
|
+
},
|
|
202
|
+
roundInfo: {
|
|
203
|
+
flex: 1,
|
|
204
|
+
},
|
|
205
|
+
roundMeta: {
|
|
206
|
+
flexDirection: 'row',
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
marginTop: 4,
|
|
209
|
+
},
|
|
210
|
+
statusDot: {
|
|
211
|
+
width: 6,
|
|
212
|
+
height: 6,
|
|
213
|
+
borderRadius: 3,
|
|
214
|
+
marginRight: 6,
|
|
215
|
+
},
|
|
216
|
+
roundCounts: {
|
|
217
|
+
marginHorizontal: 10,
|
|
218
|
+
},
|
|
219
|
+
chevron: {
|
|
220
|
+
marginLeft: 4,
|
|
221
|
+
},
|
|
222
|
+
roundBody: {
|
|
223
|
+
paddingHorizontal: 12,
|
|
224
|
+
paddingBottom: 12,
|
|
225
|
+
borderTopWidth: 1,
|
|
226
|
+
},
|
|
227
|
+
payoutRow: {
|
|
228
|
+
flexDirection: 'row',
|
|
229
|
+
alignItems: 'center',
|
|
230
|
+
paddingVertical: 8,
|
|
231
|
+
},
|
|
232
|
+
sectionLabel: {
|
|
233
|
+
marginTop: 8,
|
|
234
|
+
marginBottom: 4,
|
|
235
|
+
},
|
|
236
|
+
itemRow: {
|
|
237
|
+
flexDirection: 'row',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
paddingVertical: 4,
|
|
240
|
+
paddingLeft: 4,
|
|
241
|
+
},
|
|
242
|
+
itemName: {
|
|
243
|
+
flex: 1,
|
|
244
|
+
marginLeft: 6,
|
|
245
|
+
},
|
|
246
|
+
noResults: {
|
|
247
|
+
paddingVertical: 8,
|
|
248
|
+
textAlign: 'center',
|
|
249
|
+
},
|
|
250
|
+
});
|