@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,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaLifecycleState } from '../../helpers/lifecycleState';
|
|
6
|
+
|
|
7
|
+
export type SealedBidTab =
|
|
8
|
+
| 'items'
|
|
9
|
+
| 'my_bids'
|
|
10
|
+
| 'players'
|
|
11
|
+
| 'info'
|
|
12
|
+
| 'my_items'
|
|
13
|
+
| 'results'
|
|
14
|
+
| 'leaderboard';
|
|
15
|
+
|
|
16
|
+
interface TabDef {
|
|
17
|
+
key: SealedBidTab;
|
|
18
|
+
label: string;
|
|
19
|
+
icon: keyof typeof Ionicons.glyphMap;
|
|
20
|
+
count?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SealedBidTabBarProps {
|
|
24
|
+
activeTab: SealedBidTab;
|
|
25
|
+
onTabChange: (tab: SealedBidTab) => void;
|
|
26
|
+
itemCount: number;
|
|
27
|
+
myBidCount: number;
|
|
28
|
+
playerCount: number;
|
|
29
|
+
lifecycleState: CalcuttaLifecycleState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getTabsForState(
|
|
33
|
+
state: CalcuttaLifecycleState,
|
|
34
|
+
itemCount: number,
|
|
35
|
+
myBidCount: number,
|
|
36
|
+
playerCount: number,
|
|
37
|
+
): TabDef[] {
|
|
38
|
+
switch (state) {
|
|
39
|
+
case 'pending':
|
|
40
|
+
case 'scheduled':
|
|
41
|
+
return [
|
|
42
|
+
{ key: 'items', label: 'Items', icon: 'list-outline', count: itemCount },
|
|
43
|
+
{ key: 'players', label: 'Players', icon: 'people-outline', count: playerCount },
|
|
44
|
+
{ key: 'info', label: 'Info', icon: 'information-circle-outline' },
|
|
45
|
+
];
|
|
46
|
+
case 'auctioning':
|
|
47
|
+
return [
|
|
48
|
+
{ key: 'items', label: 'Items', icon: 'list-outline', count: itemCount },
|
|
49
|
+
{ key: 'my_bids', label: 'My Bids', icon: 'pricetag-outline', count: myBidCount },
|
|
50
|
+
{ key: 'players', label: 'Players', icon: 'people-outline', count: playerCount },
|
|
51
|
+
{ key: 'info', label: 'Info', icon: 'information-circle-outline' },
|
|
52
|
+
];
|
|
53
|
+
case 'tournament':
|
|
54
|
+
case 'completed':
|
|
55
|
+
return [
|
|
56
|
+
{ key: 'my_items', label: 'My Items', icon: 'ribbon-outline', count: myBidCount },
|
|
57
|
+
{ key: 'results', label: 'Results', icon: 'trophy-outline' },
|
|
58
|
+
{ key: 'leaderboard', label: 'Leaderboard', icon: 'podium-outline' },
|
|
59
|
+
{ key: 'info', label: 'Info', icon: 'information-circle-outline' },
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const SealedBidTabBar: React.FC<SealedBidTabBarProps> = ({
|
|
65
|
+
activeTab,
|
|
66
|
+
onTabChange,
|
|
67
|
+
itemCount,
|
|
68
|
+
myBidCount,
|
|
69
|
+
playerCount,
|
|
70
|
+
lifecycleState,
|
|
71
|
+
}) => {
|
|
72
|
+
const { theme } = useTheme();
|
|
73
|
+
|
|
74
|
+
const tabs = getTabsForState(lifecycleState, itemCount, myBidCount, playerCount);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View variant="transparent" style={[styles.container, { borderColor: theme.colors.border.subtle }]}>
|
|
78
|
+
{tabs.map((tab) => {
|
|
79
|
+
const isActive = activeTab === tab.key;
|
|
80
|
+
return (
|
|
81
|
+
<TouchableOpacity
|
|
82
|
+
key={tab.key}
|
|
83
|
+
style={styles.tab}
|
|
84
|
+
onPress={() => onTabChange(tab.key)}
|
|
85
|
+
activeOpacity={0.7}
|
|
86
|
+
>
|
|
87
|
+
<View variant="transparent" style={styles.tabContent}>
|
|
88
|
+
<Ionicons
|
|
89
|
+
name={tab.icon}
|
|
90
|
+
size={16}
|
|
91
|
+
color={isActive ? theme.colors.primary.default : theme.colors.text.tertiary}
|
|
92
|
+
/>
|
|
93
|
+
<Text
|
|
94
|
+
variant="caption"
|
|
95
|
+
bold={isActive}
|
|
96
|
+
style={{
|
|
97
|
+
color: isActive ? theme.colors.primary.default : theme.colors.text.tertiary,
|
|
98
|
+
marginLeft: 4,
|
|
99
|
+
fontSize: 11,
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{tab.label}
|
|
103
|
+
</Text>
|
|
104
|
+
{tab.count != null && tab.count > 0 && (
|
|
105
|
+
<View
|
|
106
|
+
variant="transparent"
|
|
107
|
+
style={[
|
|
108
|
+
styles.badge,
|
|
109
|
+
{
|
|
110
|
+
backgroundColor: isActive
|
|
111
|
+
? theme.colors.primary.default
|
|
112
|
+
: theme.colors.surface.elevated,
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
<Text
|
|
117
|
+
variant="caption"
|
|
118
|
+
style={{
|
|
119
|
+
color: isActive ? '#FFFFFF' : theme.colors.text.tertiary,
|
|
120
|
+
fontSize: 9,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{tab.count}
|
|
124
|
+
</Text>
|
|
125
|
+
</View>
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
{isActive && (
|
|
129
|
+
<View
|
|
130
|
+
variant="transparent"
|
|
131
|
+
style={[styles.indicator, { backgroundColor: theme.colors.primary.default }]}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</TouchableOpacity>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const styles = StyleSheet.create({
|
|
142
|
+
container: {
|
|
143
|
+
flexDirection: 'row',
|
|
144
|
+
borderBottomWidth: 1,
|
|
145
|
+
},
|
|
146
|
+
tab: {
|
|
147
|
+
flex: 1,
|
|
148
|
+
alignItems: 'center',
|
|
149
|
+
paddingVertical: 10,
|
|
150
|
+
},
|
|
151
|
+
tabContent: {
|
|
152
|
+
flexDirection: 'row',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
},
|
|
155
|
+
badge: {
|
|
156
|
+
minWidth: 16,
|
|
157
|
+
height: 16,
|
|
158
|
+
borderRadius: 8,
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
justifyContent: 'center',
|
|
161
|
+
marginLeft: 4,
|
|
162
|
+
paddingHorizontal: 4,
|
|
163
|
+
},
|
|
164
|
+
indicator: {
|
|
165
|
+
position: 'absolute',
|
|
166
|
+
bottom: 0,
|
|
167
|
+
left: '15%',
|
|
168
|
+
right: '15%',
|
|
169
|
+
height: 2,
|
|
170
|
+
borderRadius: 1,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const formatCurrency = (amount: number | string | undefined | null, market_type: string = 'FOR_MONEY'): string => {
|
|
2
|
+
const prefix = market_type === 'FOR_MONEY' ? '$' : 'E';
|
|
3
|
+
const num = Number(amount) || 0;
|
|
4
|
+
return `${prefix}${num.toFixed(2)}`;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const formatPlace = (place: number): string => {
|
|
8
|
+
if (place <= 0) return '-';
|
|
9
|
+
const suffixes = ['th', 'st', 'nd', 'rd'];
|
|
10
|
+
const v = place % 100;
|
|
11
|
+
return place + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const getStatusLabel = (status: string): string => {
|
|
15
|
+
switch (status) {
|
|
16
|
+
case 'pending': return 'Pending';
|
|
17
|
+
case 'scheduled': return 'Scheduled';
|
|
18
|
+
case 'auction_open': return 'Auction Open';
|
|
19
|
+
case 'auction_closed': return 'Auction Closed';
|
|
20
|
+
case 'inprogress': return 'In Progress';
|
|
21
|
+
case 'closed': return 'Closed';
|
|
22
|
+
case 'not_started': return 'Not Started';
|
|
23
|
+
case 'in_progress': return 'In Progress';
|
|
24
|
+
case 'active': return 'Active';
|
|
25
|
+
case 'sold': return 'Sold';
|
|
26
|
+
case 'unsold': return 'Unsold';
|
|
27
|
+
case 'eliminated': return 'Eliminated';
|
|
28
|
+
case 'advanced': return 'Advanced';
|
|
29
|
+
case 'won': return 'Won';
|
|
30
|
+
case 'placed': return 'Placed';
|
|
31
|
+
case 'outbid': return 'Outbid';
|
|
32
|
+
case 'cancelled': return 'Cancelled';
|
|
33
|
+
default: return status;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getItemStatusColor = (status: string): string => {
|
|
38
|
+
switch (status) {
|
|
39
|
+
case 'active': return '#3B82F6';
|
|
40
|
+
case 'sold': return '#10B981';
|
|
41
|
+
case 'unsold': return '#6B7280';
|
|
42
|
+
case 'eliminated': return '#EF4444';
|
|
43
|
+
case 'pending': return '#F59E0B';
|
|
44
|
+
default: return '#6B7280';
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getBidStatusColor = (status: string): string => {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 'active': return '#3B82F6';
|
|
51
|
+
case 'won': return '#10B981';
|
|
52
|
+
case 'outbid': return '#EF4444';
|
|
53
|
+
case 'cancelled': return '#6B7280';
|
|
54
|
+
default: return '#6B7280';
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calcutta lifecycle state derivation and predicates.
|
|
3
|
+
*
|
|
4
|
+
* Maps the two server fields (status + auction_status) into one of five
|
|
5
|
+
* user-facing lifecycle states so the UI can branch cleanly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type CalcuttaLifecycleState =
|
|
9
|
+
| 'pending'
|
|
10
|
+
| 'scheduled'
|
|
11
|
+
| 'auctioning'
|
|
12
|
+
| 'tournament'
|
|
13
|
+
| 'completed';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Derive the lifecycle state from competition.status and competition.auction_status.
|
|
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.
|
|
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.
|
|
26
|
+
*/
|
|
27
|
+
export function deriveLifecycleState(
|
|
28
|
+
status?: string,
|
|
29
|
+
auction_status?: string,
|
|
30
|
+
): CalcuttaLifecycleState {
|
|
31
|
+
// ── Hard gate: server confirmed auction closed ──
|
|
32
|
+
if (auction_status === 'closed') {
|
|
33
|
+
return status === 'closed' ? 'completed' : 'tournament';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!status) {
|
|
37
|
+
// No status but auction is in_progress — must be auctioning
|
|
38
|
+
if (auction_status === 'in_progress') return 'auctioning';
|
|
39
|
+
return 'pending';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
switch (status) {
|
|
43
|
+
case 'pending':
|
|
44
|
+
return 'pending';
|
|
45
|
+
case 'scheduled':
|
|
46
|
+
return 'scheduled';
|
|
47
|
+
case 'auction_open':
|
|
48
|
+
return auction_status === 'in_progress' ? 'auctioning' : 'scheduled';
|
|
49
|
+
case 'auction_closed':
|
|
50
|
+
case 'inprogress':
|
|
51
|
+
return 'tournament';
|
|
52
|
+
case 'closed':
|
|
53
|
+
return 'completed';
|
|
54
|
+
default:
|
|
55
|
+
// Unknown status — fall back to auction_status
|
|
56
|
+
if (auction_status === 'in_progress') return 'auctioning';
|
|
57
|
+
return 'pending';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when users can place / update / cancel bids */
|
|
62
|
+
export const canBid = (state: CalcuttaLifecycleState): boolean =>
|
|
63
|
+
state === 'auctioning';
|
|
64
|
+
|
|
65
|
+
/** True when escrow deposit / withdraw controls should be interactive */
|
|
66
|
+
export const canManageEscrow = (state: CalcuttaLifecycleState): boolean =>
|
|
67
|
+
state === 'scheduled' || state === 'auctioning';
|
|
68
|
+
|
|
69
|
+
/** True when round results / leaderboard data is meaningful */
|
|
70
|
+
export const showResults = (state: CalcuttaLifecycleState): boolean =>
|
|
71
|
+
state === 'tournament' || state === 'completed';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CalcuttaPayoutRuleProps } from '@bettoredge/types';
|
|
2
|
+
|
|
3
|
+
export const calculatePayoutPreview = (
|
|
4
|
+
total_pot: number,
|
|
5
|
+
rules: CalcuttaPayoutRuleProps[]
|
|
6
|
+
): { description: string; payout_pct: number; payout_amount: number }[] => {
|
|
7
|
+
return rules.map(rule => ({
|
|
8
|
+
description: rule.description || `${rule.payout_type === 'round' ? `Round ${rule.round_number}` : `${rule.placement}${getOrdinal(rule.placement || 0)} Place`}`,
|
|
9
|
+
payout_pct: Number(rule.payout_pct),
|
|
10
|
+
payout_amount: (total_pot * Number(rule.payout_pct)) / 100,
|
|
11
|
+
}));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const validatePayoutRules = (rules: CalcuttaPayoutRuleProps[]): string[] => {
|
|
15
|
+
const errors: string[] = [];
|
|
16
|
+
if (rules.length === 0) {
|
|
17
|
+
errors.push('At least one payout rule is required');
|
|
18
|
+
return errors;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const totalPct = rules.reduce((sum, r) => sum + Number(r.payout_pct), 0);
|
|
22
|
+
if (Math.abs(totalPct - 100) > 0.01) {
|
|
23
|
+
errors.push(`Payout percentages must sum to 100% (currently ${totalPct.toFixed(1)}%)`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const rule of rules) {
|
|
27
|
+
if (Number(rule.payout_pct) <= 0) {
|
|
28
|
+
errors.push(`Payout percentage must be greater than 0`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return errors;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getOrdinal = (n: number): string => {
|
|
36
|
+
const suffixes = ['th', 'st', 'nd', 'rd'];
|
|
37
|
+
const v = n % 100;
|
|
38
|
+
return suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0];
|
|
39
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CalcuttaCompetitionProps, CalcuttaEscrowProps } from '@bettoredge/types';
|
|
2
|
+
|
|
3
|
+
export const validateBidAmount = (
|
|
4
|
+
amount: number,
|
|
5
|
+
current_bid: number,
|
|
6
|
+
min_bid: number,
|
|
7
|
+
bid_increment: number,
|
|
8
|
+
auction_type: string,
|
|
9
|
+
escrow?: CalcuttaEscrowProps,
|
|
10
|
+
existing_bid_amount?: number
|
|
11
|
+
): string[] => {
|
|
12
|
+
const errors: string[] = [];
|
|
13
|
+
|
|
14
|
+
if (isNaN(amount) || amount <= 0) {
|
|
15
|
+
errors.push('Please enter a valid bid amount');
|
|
16
|
+
return errors;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (amount < min_bid) {
|
|
20
|
+
errors.push(`Minimum bid is $${min_bid.toFixed(2)}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (amount <= current_bid) {
|
|
24
|
+
errors.push(`Bid must exceed current bid of $${current_bid.toFixed(2)}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (auction_type === 'live' && amount < current_bid + bid_increment) {
|
|
28
|
+
errors.push(`Minimum bid is $${(current_bid + bid_increment).toFixed(2)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (escrow) {
|
|
32
|
+
const available = Number(escrow.escrow_balance) + (existing_bid_amount || 0);
|
|
33
|
+
if (amount > available) {
|
|
34
|
+
errors.push(`Insufficient escrow balance. Available: $${available.toFixed(2)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return errors;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const validateEscrowDeposit = (amount: number, max_escrow?: number, net_deposited?: number): string[] => {
|
|
42
|
+
const errors: string[] = [];
|
|
43
|
+
if (isNaN(amount) || amount <= 0) {
|
|
44
|
+
errors.push('Please enter a valid amount');
|
|
45
|
+
}
|
|
46
|
+
if (max_escrow != null && max_escrow > 0 && net_deposited != null) {
|
|
47
|
+
const remaining = max_escrow - net_deposited;
|
|
48
|
+
if (amount > remaining) {
|
|
49
|
+
errors.push(`Budget cap is $${max_escrow.toFixed(2)}. You can deposit up to $${Math.max(0, remaining).toFixed(2)} more.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return errors;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const validateEscrowWithdraw = (amount: number, available: number): string[] => {
|
|
56
|
+
const errors: string[] = [];
|
|
57
|
+
if (isNaN(amount) || amount <= 0) {
|
|
58
|
+
errors.push('Please enter a valid amount');
|
|
59
|
+
}
|
|
60
|
+
if (amount > available) {
|
|
61
|
+
errors.push(`Maximum withdrawal is $${available.toFixed(2)}`);
|
|
62
|
+
}
|
|
63
|
+
return errors;
|
|
64
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { getCalcuttaAuctionStatus } from '@bettoredge/api';
|
|
3
|
+
import type {
|
|
4
|
+
CalcuttaCompetitionProps,
|
|
5
|
+
CalcuttaAuctionItemProps,
|
|
6
|
+
CalcuttaBidProps,
|
|
7
|
+
} from '@bettoredge/types';
|
|
8
|
+
|
|
9
|
+
export interface CalcuttaAuctionState {
|
|
10
|
+
loading: boolean;
|
|
11
|
+
competition?: CalcuttaCompetitionProps;
|
|
12
|
+
items: CalcuttaAuctionItemProps[];
|
|
13
|
+
bids: CalcuttaBidProps[];
|
|
14
|
+
my_bids: CalcuttaBidProps[];
|
|
15
|
+
paused: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useCalcuttaAuction = (calcutta_competition_id?: string, poll_interval: number = 5000, player_id?: string) => {
|
|
19
|
+
const [state, setState] = useState<CalcuttaAuctionState>({
|
|
20
|
+
loading: false,
|
|
21
|
+
items: [],
|
|
22
|
+
bids: [],
|
|
23
|
+
my_bids: [],
|
|
24
|
+
paused: false,
|
|
25
|
+
});
|
|
26
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
27
|
+
|
|
28
|
+
const fetchStatus = useCallback(async () => {
|
|
29
|
+
if (!calcutta_competition_id) return;
|
|
30
|
+
try {
|
|
31
|
+
const resp = await getCalcuttaAuctionStatus(calcutta_competition_id);
|
|
32
|
+
setState({
|
|
33
|
+
loading: false,
|
|
34
|
+
competition: resp.competition,
|
|
35
|
+
items: resp.items,
|
|
36
|
+
bids: resp.bids,
|
|
37
|
+
my_bids: resp.my_bids,
|
|
38
|
+
paused: resp.competition?.auction_status === 'paused',
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
// Silently fail on poll
|
|
42
|
+
}
|
|
43
|
+
}, [calcutta_competition_id]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!calcutta_competition_id) return;
|
|
47
|
+
setState(prev => ({ ...prev, loading: true }));
|
|
48
|
+
fetchStatus();
|
|
49
|
+
|
|
50
|
+
// Start polling
|
|
51
|
+
intervalRef.current = setInterval(fetchStatus, poll_interval);
|
|
52
|
+
return () => {
|
|
53
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
54
|
+
};
|
|
55
|
+
}, [calcutta_competition_id, poll_interval, fetchStatus]);
|
|
56
|
+
|
|
57
|
+
const stopPolling = useCallback(() => {
|
|
58
|
+
if (intervalRef.current) {
|
|
59
|
+
clearInterval(intervalRef.current);
|
|
60
|
+
intervalRef.current = null;
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
// Socket event handlers for live mode — update local state directly
|
|
65
|
+
const handleBidUpdate = useCallback((data: { item: CalcuttaAuctionItemProps; bid: CalcuttaBidProps; item_deadline?: string }) => {
|
|
66
|
+
setState(prev => {
|
|
67
|
+
const newState = {
|
|
68
|
+
...prev,
|
|
69
|
+
items: prev.items.map(i =>
|
|
70
|
+
i.calcutta_auction_item_id === data.item.calcutta_auction_item_id ? data.item : i
|
|
71
|
+
),
|
|
72
|
+
bids: [...prev.bids.filter(b => b.calcutta_bid_id !== data.bid.calcutta_bid_id), data.bid],
|
|
73
|
+
my_bids: prev.my_bids,
|
|
74
|
+
};
|
|
75
|
+
// Update my_bids if this bid belongs to the current user
|
|
76
|
+
if (player_id && data.bid.player_id === player_id) {
|
|
77
|
+
newState.my_bids = [
|
|
78
|
+
...prev.my_bids.filter(b => b.calcutta_auction_item_id !== data.bid.calcutta_auction_item_id),
|
|
79
|
+
data.bid,
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
return newState;
|
|
83
|
+
});
|
|
84
|
+
}, [player_id]);
|
|
85
|
+
|
|
86
|
+
const handleItemActive = useCallback((data: { item: CalcuttaAuctionItemProps; item_deadline?: string }) => {
|
|
87
|
+
setState(prev => ({
|
|
88
|
+
...prev,
|
|
89
|
+
competition: prev.competition ? { ...prev.competition, current_auction_item_id: data.item.calcutta_auction_item_id } : prev.competition,
|
|
90
|
+
items: prev.items.map(i =>
|
|
91
|
+
i.calcutta_auction_item_id === data.item.calcutta_auction_item_id ? data.item : i
|
|
92
|
+
),
|
|
93
|
+
}));
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const handleItemSold = useCallback((data: { item: CalcuttaAuctionItemProps; winning_bid: CalcuttaBidProps; winning_player_id: string }) => {
|
|
97
|
+
setState(prev => ({
|
|
98
|
+
...prev,
|
|
99
|
+
items: prev.items.map(i =>
|
|
100
|
+
i.calcutta_auction_item_id === data.item.calcutta_auction_item_id
|
|
101
|
+
? { ...data.item, winning_bid: data.winning_bid?.bid_amount ?? data.item.winning_bid, winning_player_id: data.winning_player_id }
|
|
102
|
+
: i
|
|
103
|
+
),
|
|
104
|
+
}));
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
const handleItemUnsold = useCallback((data: { item: CalcuttaAuctionItemProps }) => {
|
|
108
|
+
setState(prev => ({
|
|
109
|
+
...prev,
|
|
110
|
+
items: prev.items.map(i =>
|
|
111
|
+
i.calcutta_auction_item_id === data.item.calcutta_auction_item_id ? data.item : i
|
|
112
|
+
),
|
|
113
|
+
}));
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const handleAuctionPaused = useCallback(() => {
|
|
117
|
+
setState(prev => ({
|
|
118
|
+
...prev,
|
|
119
|
+
paused: true,
|
|
120
|
+
competition: prev.competition ? { ...prev.competition, auction_status: 'paused' } : prev.competition,
|
|
121
|
+
// Clear item deadline on active item
|
|
122
|
+
items: prev.items.map(i =>
|
|
123
|
+
i.calcutta_auction_item_id === prev.competition?.current_auction_item_id
|
|
124
|
+
? { ...i, item_deadline: null }
|
|
125
|
+
: i
|
|
126
|
+
),
|
|
127
|
+
}));
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const handleAuctionResumed = useCallback((data: { item?: CalcuttaAuctionItemProps; item_deadline?: string }) => {
|
|
131
|
+
setState(prev => ({
|
|
132
|
+
...prev,
|
|
133
|
+
paused: false,
|
|
134
|
+
competition: prev.competition ? { ...prev.competition, auction_status: 'in_progress' } : prev.competition,
|
|
135
|
+
items: data.item
|
|
136
|
+
? prev.items.map(i =>
|
|
137
|
+
i.calcutta_auction_item_id === data.item!.calcutta_auction_item_id ? data.item! : i
|
|
138
|
+
)
|
|
139
|
+
: prev.items,
|
|
140
|
+
}));
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
const handleAuctionClosed = useCallback((data: { competition: CalcuttaCompetitionProps }) => {
|
|
144
|
+
setState(prev => ({
|
|
145
|
+
...prev,
|
|
146
|
+
competition: data.competition,
|
|
147
|
+
paused: false,
|
|
148
|
+
}));
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...state,
|
|
153
|
+
refresh: fetchStatus,
|
|
154
|
+
stopPolling,
|
|
155
|
+
// Socket event handlers for CalcuttaAuction to wire up
|
|
156
|
+
handleBidUpdate,
|
|
157
|
+
handleItemActive,
|
|
158
|
+
handleItemSold,
|
|
159
|
+
handleItemUnsold,
|
|
160
|
+
handleAuctionPaused,
|
|
161
|
+
handleAuctionResumed,
|
|
162
|
+
handleAuctionClosed,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { placeCalcuttaBid, cancelCalcuttaBid } from '@bettoredge/api';
|
|
3
|
+
import type { CalcuttaBidProps } from '@bettoredge/types';
|
|
4
|
+
|
|
5
|
+
export interface CalcuttaBidState {
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
last_bid?: CalcuttaBidProps;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const useCalcuttaBid = () => {
|
|
12
|
+
const [state, setState] = useState<CalcuttaBidState>({ loading: false });
|
|
13
|
+
|
|
14
|
+
const placeBid = useCallback(async (calcutta_auction_item_id: string, amount: number) => {
|
|
15
|
+
setState({ loading: true });
|
|
16
|
+
try {
|
|
17
|
+
const resp = await placeCalcuttaBid({ calcutta_auction_item_id, amount });
|
|
18
|
+
setState({ loading: false, last_bid: resp.bid });
|
|
19
|
+
return resp.bid;
|
|
20
|
+
} catch (e: any) {
|
|
21
|
+
setState({ loading: false, error: e.message });
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const cancelBid = useCallback(async (calcutta_bid_id: string) => {
|
|
27
|
+
setState(prev => ({ ...prev, loading: true }));
|
|
28
|
+
try {
|
|
29
|
+
const resp = await cancelCalcuttaBid(calcutta_bid_id);
|
|
30
|
+
setState({ loading: false, last_bid: resp.bid });
|
|
31
|
+
return resp.bid;
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
setState(prev => ({ ...prev, loading: false, error: e.message }));
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const clearError = useCallback(() => {
|
|
39
|
+
setState(prev => ({ ...prev, error: undefined }));
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return { ...state, placeBid, cancelBid, clearError };
|
|
43
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { getCalcuttaCompetition } from '@bettoredge/api';
|
|
3
|
+
import type {
|
|
4
|
+
CalcuttaCompetitionProps,
|
|
5
|
+
CalcuttaRoundProps,
|
|
6
|
+
CalcuttaAuctionItemProps,
|
|
7
|
+
CalcuttaParticipantProps,
|
|
8
|
+
CalcuttaPayoutRuleProps,
|
|
9
|
+
CalcuttaItemResultProps,
|
|
10
|
+
CalcuttaBidProps,
|
|
11
|
+
CalcuttaEscrowProps,
|
|
12
|
+
} from '@bettoredge/types';
|
|
13
|
+
|
|
14
|
+
export interface CalcuttaCompetitionState {
|
|
15
|
+
loading: boolean;
|
|
16
|
+
competition?: CalcuttaCompetitionProps;
|
|
17
|
+
rounds: CalcuttaRoundProps[];
|
|
18
|
+
items: CalcuttaAuctionItemProps[];
|
|
19
|
+
participants: CalcuttaParticipantProps[];
|
|
20
|
+
payout_rules: CalcuttaPayoutRuleProps[];
|
|
21
|
+
item_results: CalcuttaItemResultProps[];
|
|
22
|
+
my_bids: CalcuttaBidProps[];
|
|
23
|
+
my_escrow?: CalcuttaEscrowProps;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const useCalcuttaCompetition = (calcutta_competition_id?: string) => {
|
|
27
|
+
const [state, setState] = useState<CalcuttaCompetitionState>({
|
|
28
|
+
loading: false,
|
|
29
|
+
rounds: [],
|
|
30
|
+
items: [],
|
|
31
|
+
participants: [],
|
|
32
|
+
payout_rules: [],
|
|
33
|
+
item_results: [],
|
|
34
|
+
my_bids: [],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const fetchCompetition = useCallback(async () => {
|
|
38
|
+
if (!calcutta_competition_id) return;
|
|
39
|
+
setState(prev => ({ ...prev, loading: true }));
|
|
40
|
+
try {
|
|
41
|
+
const resp = await getCalcuttaCompetition(calcutta_competition_id);
|
|
42
|
+
setState({
|
|
43
|
+
loading: false,
|
|
44
|
+
competition: resp.competition,
|
|
45
|
+
rounds: resp.rounds,
|
|
46
|
+
items: resp.items,
|
|
47
|
+
participants: resp.participants,
|
|
48
|
+
payout_rules: resp.payout_rules,
|
|
49
|
+
item_results: resp.item_results,
|
|
50
|
+
my_bids: resp.my_bids,
|
|
51
|
+
my_escrow: resp.my_escrow,
|
|
52
|
+
});
|
|
53
|
+
} catch {
|
|
54
|
+
setState(prev => ({ ...prev, loading: false }));
|
|
55
|
+
}
|
|
56
|
+
}, [calcutta_competition_id]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
fetchCompetition();
|
|
60
|
+
}, [fetchCompetition]);
|
|
61
|
+
|
|
62
|
+
return { ...state, refresh: fetchCompetition };
|
|
63
|
+
};
|