@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.
Files changed (38) hide show
  1. package/package.json +46 -0
  2. package/src/components/CalcuttaAuction.tsx +453 -0
  3. package/src/components/CalcuttaAuctionItem.tsx +292 -0
  4. package/src/components/CalcuttaBidInput.tsx +214 -0
  5. package/src/components/CalcuttaCard.tsx +131 -0
  6. package/src/components/CalcuttaDetail.tsx +377 -0
  7. package/src/components/CalcuttaEscrow.tsx +464 -0
  8. package/src/components/CalcuttaItemResults.tsx +207 -0
  9. package/src/components/CalcuttaLeaderboard.tsx +179 -0
  10. package/src/components/CalcuttaPayoutPreview.tsx +194 -0
  11. package/src/components/CalcuttaRoundResults.tsx +250 -0
  12. package/src/components/CalcuttaTemplateSelector.tsx +124 -0
  13. package/src/components/sealed/AuctionResultsModal.tsx +165 -0
  14. package/src/components/sealed/EscrowBottomSheet.tsx +185 -0
  15. package/src/components/sealed/SealedBidAuction.tsx +541 -0
  16. package/src/components/sealed/SealedBidHeader.tsx +116 -0
  17. package/src/components/sealed/SealedBidInfoTab.tsx +247 -0
  18. package/src/components/sealed/SealedBidItemCard.tsx +385 -0
  19. package/src/components/sealed/SealedBidItemsTab.tsx +235 -0
  20. package/src/components/sealed/SealedBidMyBidsTab.tsx +512 -0
  21. package/src/components/sealed/SealedBidPlayersTab.tsx +220 -0
  22. package/src/components/sealed/SealedBidStatusBar.tsx +415 -0
  23. package/src/components/sealed/SealedBidTabBar.tsx +172 -0
  24. package/src/helpers/formatting.ts +56 -0
  25. package/src/helpers/lifecycleState.ts +71 -0
  26. package/src/helpers/payout.ts +39 -0
  27. package/src/helpers/validation.ts +64 -0
  28. package/src/hooks/useCalcuttaAuction.ts +164 -0
  29. package/src/hooks/useCalcuttaBid.ts +43 -0
  30. package/src/hooks/useCalcuttaCompetition.ts +63 -0
  31. package/src/hooks/useCalcuttaEscrow.ts +52 -0
  32. package/src/hooks/useCalcuttaItemImages.ts +79 -0
  33. package/src/hooks/useCalcuttaPlayers.ts +46 -0
  34. package/src/hooks/useCalcuttaResults.ts +58 -0
  35. package/src/hooks/useCalcuttaSocket.ts +131 -0
  36. package/src/hooks/useCalcuttaTemplates.ts +36 -0
  37. package/src/index.ts +74 -0
  38. 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
+ };