@bettoredge/calcutta 0.3.0 → 0.4.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 +3 -2
- package/src/components/AuctionCompleteOverlay.tsx +306 -0
- package/src/components/AuctionCountdownOverlay.tsx +178 -0
- package/src/components/AuctionInfoChips.tsx +105 -0
- package/src/components/AuctionPausedOverlay.tsx +154 -0
- package/src/components/CalcuttaActionCard.tsx +291 -0
- package/src/components/CalcuttaAuction.tsx +682 -281
- package/src/components/CalcuttaAuctionItem.tsx +195 -106
- package/src/components/CalcuttaBidInput.tsx +130 -139
- package/src/components/CalcuttaCountdown.tsx +183 -0
- package/src/components/CalcuttaDetail.tsx +289 -143
- package/src/components/CalcuttaEscrow.tsx +49 -13
- package/src/components/CalcuttaLeaderboard.tsx +2 -2
- package/src/components/EscrowWidget.tsx +176 -0
- package/src/components/ItemSoldCelebration.tsx +288 -0
- package/src/components/SweepstakesReveal.tsx +1 -1
- package/src/components/sealed/SealedBidAuction.tsx +7 -9
- package/src/components/sealed/SealedBidHeader.tsx +3 -3
- package/src/components/sealed/SealedBidItemCard.tsx +9 -9
- package/src/components/sealed/SealedBidItemsTab.tsx +2 -1
- package/src/components/sealed/SealedBidMyBidsTab.tsx +3 -3
- package/src/components/sealed/SealedBidPlayersTab.tsx +2 -2
- package/src/components/sealed/SealedBidStatusBar.tsx +1 -1
- package/src/components/sealed/SealedBidTabBar.tsx +2 -0
- package/src/hooks/useCalcuttaAuction.ts +16 -2
- package/src/hooks/useCalcuttaEscrow.ts +5 -1
- package/src/hooks/useCalcuttaSocket.ts +80 -82
- package/src/index.ts +18 -0
|
@@ -66,10 +66,25 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
|
|
|
66
66
|
const hasBalance = player_balance != null;
|
|
67
67
|
const insufficientBalance = hasBalance && action === 'transfer_in' && numericAmount > Number(player_balance);
|
|
68
68
|
const shortfall = insufficientBalance ? numericAmount - Number(player_balance) : 0;
|
|
69
|
+
const exceedsBudget = action === 'transfer_in' && budgetRemaining !== null && numericAmount > budgetRemaining;
|
|
70
|
+
const exceedsWithdraw = action === 'transfer_out' && numericAmount > available;
|
|
71
|
+
const [actionError, setActionError] = useState('');
|
|
72
|
+
|
|
73
|
+
const isInvalidAmount = numericAmount <= 0 || exceedsBudget || exceedsWithdraw || (insufficientBalance && !onDepositFunds);
|
|
74
|
+
|
|
75
|
+
const getValidationMessage = (): string => {
|
|
76
|
+
if (numericAmount <= 0) return '';
|
|
77
|
+
if (exceedsBudget) return `Budget cap: ${formatCurrency(Number(max_escrow), market_type)}. You can deposit up to ${formatCurrency(budgetRemaining ?? 0, market_type)} more.`;
|
|
78
|
+
if (exceedsWithdraw) return `You only have ${formatCurrency(available, market_type)} available to withdraw.`;
|
|
79
|
+
if (insufficientBalance && !onDepositFunds) return `Insufficient wallet balance. You have ${formatCurrency(player_balance, market_type)}.`;
|
|
80
|
+
return '';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const validationMessage = getValidationMessage();
|
|
69
84
|
|
|
70
85
|
const handleAction = async () => {
|
|
71
86
|
if (isNaN(numericAmount) || numericAmount <= 0) return;
|
|
72
|
-
|
|
87
|
+
setActionError('');
|
|
73
88
|
if (insufficientBalance && onDepositFunds) {
|
|
74
89
|
onDepositFunds(Math.ceil(shortfall * 100) / 100);
|
|
75
90
|
return;
|
|
@@ -84,12 +99,22 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
|
|
|
84
99
|
if (result) {
|
|
85
100
|
onUpdate(result);
|
|
86
101
|
setAmount('');
|
|
102
|
+
setActionError('');
|
|
87
103
|
}
|
|
88
|
-
} catch {
|
|
104
|
+
} catch (e: any) {
|
|
105
|
+
setActionError(e?.message || 'Transfer failed. Please try again.');
|
|
106
|
+
}
|
|
89
107
|
};
|
|
90
108
|
|
|
91
109
|
const handleQuickAmount = (amt: number) => {
|
|
92
|
-
|
|
110
|
+
// Cap quick amount to budget remaining for deposits
|
|
111
|
+
if (action === 'transfer_in' && budgetRemaining !== null) {
|
|
112
|
+
setAmount(String(Math.min(amt, budgetRemaining)));
|
|
113
|
+
} else if (action === 'transfer_out') {
|
|
114
|
+
setAmount(String(Math.min(amt, available)));
|
|
115
|
+
} else {
|
|
116
|
+
setAmount(String(amt));
|
|
117
|
+
}
|
|
93
118
|
};
|
|
94
119
|
|
|
95
120
|
// ============================================
|
|
@@ -278,14 +303,14 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
|
|
|
278
303
|
</View>
|
|
279
304
|
<TouchableOpacity
|
|
280
305
|
style={[styles.submitBtn, {
|
|
281
|
-
backgroundColor: insufficientBalance
|
|
306
|
+
backgroundColor: insufficientBalance && onDepositFunds
|
|
282
307
|
? theme.colors.status.error
|
|
283
|
-
: (loading ||
|
|
308
|
+
: (loading || isInvalidAmount)
|
|
284
309
|
? theme.colors.surface.elevated
|
|
285
310
|
: theme.colors.primary.default,
|
|
286
311
|
}]}
|
|
287
312
|
onPress={handleAction}
|
|
288
|
-
disabled={loading ||
|
|
313
|
+
disabled={loading || (isInvalidAmount && !(insufficientBalance && onDepositFunds))}
|
|
289
314
|
activeOpacity={0.7}
|
|
290
315
|
>
|
|
291
316
|
{loading ? (
|
|
@@ -295,13 +320,13 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
|
|
|
295
320
|
<Ionicons
|
|
296
321
|
name={action === 'transfer_in' ? 'arrow-down-circle' : 'arrow-up-circle'}
|
|
297
322
|
size={20}
|
|
298
|
-
color={
|
|
323
|
+
color={isInvalidAmount ? theme.colors.text.tertiary : '#FFFFFF'}
|
|
299
324
|
/>
|
|
300
325
|
<Text
|
|
301
326
|
variant="body"
|
|
302
327
|
bold
|
|
303
328
|
style={{
|
|
304
|
-
color:
|
|
329
|
+
color: isInvalidAmount ? theme.colors.text.tertiary : '#FFFFFF',
|
|
305
330
|
marginLeft: 6,
|
|
306
331
|
}}
|
|
307
332
|
>
|
|
@@ -316,8 +341,18 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
|
|
|
316
341
|
</TouchableOpacity>
|
|
317
342
|
</View>
|
|
318
343
|
|
|
319
|
-
{/*
|
|
320
|
-
{
|
|
344
|
+
{/* Validation message (budget cap, withdraw limit, etc.) */}
|
|
345
|
+
{validationMessage ? (
|
|
346
|
+
<View variant="transparent" style={[styles.warningRow, { backgroundColor: '#D9770615' }]}>
|
|
347
|
+
<Ionicons name="information-circle-outline" size={16} color="#D97706" />
|
|
348
|
+
<Text variant="caption" style={{ color: '#D97706', marginLeft: 8, flex: 1 }}>
|
|
349
|
+
{validationMessage}
|
|
350
|
+
</Text>
|
|
351
|
+
</View>
|
|
352
|
+
) : null}
|
|
353
|
+
|
|
354
|
+
{/* Insufficient wallet balance */}
|
|
355
|
+
{insufficientBalance && !validationMessage && (
|
|
321
356
|
<View variant="transparent" style={[styles.warningRow, { backgroundColor: theme.colors.status.error + '10' }]}>
|
|
322
357
|
<Ionicons name="warning-outline" size={16} color={theme.colors.status.error} />
|
|
323
358
|
<Text variant="caption" style={{ color: theme.colors.status.error, marginLeft: 8, flex: 1 }}>
|
|
@@ -326,12 +361,12 @@ export const CalcuttaEscrow: React.FC<CalcuttaEscrowComponentProps> = ({
|
|
|
326
361
|
</View>
|
|
327
362
|
)}
|
|
328
363
|
|
|
329
|
-
{/*
|
|
330
|
-
{error && (
|
|
364
|
+
{/* API error */}
|
|
365
|
+
{(error || actionError) && (
|
|
331
366
|
<View variant="transparent" style={[styles.warningRow, { backgroundColor: theme.colors.status.error + '10' }]}>
|
|
332
367
|
<Ionicons name="alert-circle-outline" size={16} color={theme.colors.status.error} />
|
|
333
368
|
<Text variant="caption" style={{ color: theme.colors.status.error, marginLeft: 8, flex: 1 }}>
|
|
334
|
-
{error}
|
|
369
|
+
{actionError || error}
|
|
335
370
|
</Text>
|
|
336
371
|
</View>
|
|
337
372
|
)}
|
|
@@ -442,6 +477,7 @@ const styles = StyleSheet.create({
|
|
|
442
477
|
input: {
|
|
443
478
|
flex: 1,
|
|
444
479
|
fontSize: 18,
|
|
480
|
+
lineHeight: 24,
|
|
445
481
|
height: 48,
|
|
446
482
|
padding: 0,
|
|
447
483
|
},
|
|
@@ -74,7 +74,7 @@ export const CalcuttaLeaderboard: React.FC<CalcuttaLeaderboardProps> = ({
|
|
|
74
74
|
<Image source={{ uri: profilePic }} style={styles.avatar} />
|
|
75
75
|
) : (
|
|
76
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 }}>
|
|
77
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 10, lineHeight: 13 }}>
|
|
78
78
|
{username.charAt(0).toUpperCase()}
|
|
79
79
|
</Text>
|
|
80
80
|
</View>
|
|
@@ -84,7 +84,7 @@ export const CalcuttaLeaderboard: React.FC<CalcuttaLeaderboardProps> = ({
|
|
|
84
84
|
<Text variant="body" bold={isMe} numberOfLines={1} style={{ flex: 1 }}>{username}</Text>
|
|
85
85
|
{isMe && (
|
|
86
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>
|
|
87
|
+
<Text variant="caption" bold style={{ color: theme.colors.primary.default, fontSize: 9, lineHeight: 12 }}>YOU</Text>
|
|
88
88
|
</View>
|
|
89
89
|
)}
|
|
90
90
|
</View>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { StyleSheet, TouchableOpacity, LayoutAnimation, Platform, UIManager } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
import type { CalcuttaEscrowProps } from '@bettoredge/types';
|
|
6
|
+
import { useCalcuttaEscrow } from '../hooks/useCalcuttaEscrow';
|
|
7
|
+
import { formatCurrency } from '../helpers/formatting';
|
|
8
|
+
import { CalcuttaEscrow } from './CalcuttaEscrow';
|
|
9
|
+
|
|
10
|
+
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
|
11
|
+
UIManager.setLayoutAnimationEnabledExperimental(true);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EscrowWidgetProps {
|
|
15
|
+
calcutta_competition_id: string;
|
|
16
|
+
market_type: string;
|
|
17
|
+
max_escrow?: number;
|
|
18
|
+
player_balance?: number;
|
|
19
|
+
onDepositFunds?: (amount: number) => void;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
onUpdate?: () => void;
|
|
22
|
+
initialExpanded?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const EscrowWidget: React.FC<EscrowWidgetProps> = ({
|
|
26
|
+
calcutta_competition_id,
|
|
27
|
+
market_type,
|
|
28
|
+
max_escrow,
|
|
29
|
+
player_balance,
|
|
30
|
+
onDepositFunds,
|
|
31
|
+
readOnly,
|
|
32
|
+
onUpdate,
|
|
33
|
+
initialExpanded,
|
|
34
|
+
}) => {
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
const { escrow, fetchEscrow } = useCalcuttaEscrow(calcutta_competition_id);
|
|
37
|
+
const [expanded, setExpanded] = useState(initialExpanded ?? false);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (initialExpanded) setExpanded(true);
|
|
41
|
+
}, [initialExpanded]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
fetchEscrow();
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const available = Number(escrow?.escrow_balance ?? 0);
|
|
48
|
+
const committed = Number(escrow?.committed_balance ?? 0);
|
|
49
|
+
const hasFunds = available > 0 || committed > 0;
|
|
50
|
+
|
|
51
|
+
const toggle = () => {
|
|
52
|
+
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
|
53
|
+
setExpanded(!expanded);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<View variant="transparent" style={[styles.container, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}>
|
|
58
|
+
{/* Collapsed header — always visible */}
|
|
59
|
+
<TouchableOpacity
|
|
60
|
+
style={styles.header}
|
|
61
|
+
onPress={readOnly ? undefined : toggle}
|
|
62
|
+
activeOpacity={readOnly ? 1 : 0.7}
|
|
63
|
+
>
|
|
64
|
+
<View variant="transparent" style={[styles.iconCircle, { backgroundColor: hasFunds ? theme.colors.status.successSubtle : theme.colors.primary.subtle }]}>
|
|
65
|
+
<Ionicons
|
|
66
|
+
name="wallet-outline"
|
|
67
|
+
size={18}
|
|
68
|
+
color={hasFunds ? theme.colors.status.success : theme.colors.primary.default}
|
|
69
|
+
/>
|
|
70
|
+
</View>
|
|
71
|
+
<View variant="transparent" style={styles.headerText}>
|
|
72
|
+
<Text variant="body" bold>
|
|
73
|
+
{formatCurrency(available, market_type)}
|
|
74
|
+
<Text variant="caption" color="tertiary"> available</Text>
|
|
75
|
+
</Text>
|
|
76
|
+
{committed > 0 && (
|
|
77
|
+
<Text variant="caption" color="tertiary">
|
|
78
|
+
{formatCurrency(committed, market_type)} committed
|
|
79
|
+
</Text>
|
|
80
|
+
)}
|
|
81
|
+
</View>
|
|
82
|
+
{!readOnly && (
|
|
83
|
+
<View variant="transparent" style={[styles.addBtn, { backgroundColor: theme.colors.primary.default }]}>
|
|
84
|
+
<Ionicons name={expanded ? 'chevron-up' : 'add'} size={18} color="#FFFFFF" />
|
|
85
|
+
{!expanded && (
|
|
86
|
+
<Text variant="caption" bold style={{ color: '#FFFFFF', marginLeft: 4 }}>
|
|
87
|
+
{hasFunds ? 'Manage' : 'Add Funds'}
|
|
88
|
+
</Text>
|
|
89
|
+
)}
|
|
90
|
+
</View>
|
|
91
|
+
)}
|
|
92
|
+
</TouchableOpacity>
|
|
93
|
+
|
|
94
|
+
{/* Budget cap bar (always visible if applicable) */}
|
|
95
|
+
{max_escrow != null && Number(max_escrow) > 0 && (
|
|
96
|
+
<View variant="transparent" style={styles.budgetBar}>
|
|
97
|
+
<View variant="transparent" style={[styles.barTrack, { backgroundColor: theme.colors.surface.base }]}>
|
|
98
|
+
<View variant="transparent" style={[styles.barFill, {
|
|
99
|
+
width: `${Math.min(100, ((Number(escrow?.total_deposited ?? 0) - Number(escrow?.total_withdrawn ?? 0)) / Number(max_escrow)) * 100)}%`,
|
|
100
|
+
backgroundColor: theme.colors.primary.default,
|
|
101
|
+
}]} />
|
|
102
|
+
</View>
|
|
103
|
+
<Text variant="caption" color="tertiary" style={{ marginTop: 4 }}>
|
|
104
|
+
Budget: {formatCurrency(Number(max_escrow), market_type)}
|
|
105
|
+
</Text>
|
|
106
|
+
</View>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{/* Expanded: full escrow management */}
|
|
110
|
+
{expanded && !readOnly && (
|
|
111
|
+
<View variant="transparent" style={styles.expandedContent}>
|
|
112
|
+
<CalcuttaEscrow
|
|
113
|
+
calcutta_competition_id={calcutta_competition_id}
|
|
114
|
+
escrow={escrow ?? undefined}
|
|
115
|
+
market_type={market_type}
|
|
116
|
+
max_escrow={max_escrow}
|
|
117
|
+
player_balance={player_balance}
|
|
118
|
+
onDepositFunds={onDepositFunds}
|
|
119
|
+
onUpdate={(updated) => {
|
|
120
|
+
fetchEscrow();
|
|
121
|
+
onUpdate?.();
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
</View>
|
|
125
|
+
)}
|
|
126
|
+
</View>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const styles = StyleSheet.create({
|
|
131
|
+
container: {
|
|
132
|
+
borderRadius: 14,
|
|
133
|
+
borderWidth: 1,
|
|
134
|
+
overflow: 'hidden',
|
|
135
|
+
},
|
|
136
|
+
header: {
|
|
137
|
+
flexDirection: 'row',
|
|
138
|
+
alignItems: 'center',
|
|
139
|
+
padding: 14,
|
|
140
|
+
},
|
|
141
|
+
iconCircle: {
|
|
142
|
+
width: 36,
|
|
143
|
+
height: 36,
|
|
144
|
+
borderRadius: 18,
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
justifyContent: 'center',
|
|
147
|
+
},
|
|
148
|
+
headerText: {
|
|
149
|
+
flex: 1,
|
|
150
|
+
marginLeft: 12,
|
|
151
|
+
},
|
|
152
|
+
addBtn: {
|
|
153
|
+
flexDirection: 'row',
|
|
154
|
+
alignItems: 'center',
|
|
155
|
+
paddingHorizontal: 12,
|
|
156
|
+
paddingVertical: 6,
|
|
157
|
+
borderRadius: 20,
|
|
158
|
+
},
|
|
159
|
+
budgetBar: {
|
|
160
|
+
paddingHorizontal: 14,
|
|
161
|
+
paddingBottom: 12,
|
|
162
|
+
},
|
|
163
|
+
barTrack: {
|
|
164
|
+
height: 4,
|
|
165
|
+
borderRadius: 2,
|
|
166
|
+
overflow: 'hidden',
|
|
167
|
+
},
|
|
168
|
+
barFill: {
|
|
169
|
+
height: 4,
|
|
170
|
+
borderRadius: 2,
|
|
171
|
+
},
|
|
172
|
+
expandedContent: {
|
|
173
|
+
borderTopWidth: 1,
|
|
174
|
+
borderTopColor: 'rgba(128,128,128,0.15)',
|
|
175
|
+
},
|
|
176
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { StyleSheet, Animated, Image } 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 { formatCurrency } from '../helpers/formatting';
|
|
7
|
+
import { CalcuttaCountdown } from './CalcuttaCountdown';
|
|
8
|
+
|
|
9
|
+
export interface ItemSoldCelebrationProps {
|
|
10
|
+
visible: boolean;
|
|
11
|
+
item?: CalcuttaAuctionItemProps;
|
|
12
|
+
winnerName?: string;
|
|
13
|
+
winnerProfilePic?: string;
|
|
14
|
+
winningBid?: number;
|
|
15
|
+
isMe?: boolean;
|
|
16
|
+
marketType?: string;
|
|
17
|
+
/** Next item coming up (shown at bottom of modal) */
|
|
18
|
+
nextItem?: CalcuttaAuctionItemProps;
|
|
19
|
+
nextItemDeadline?: string;
|
|
20
|
+
/** Auto-dismiss after this many ms (default 5000) */
|
|
21
|
+
autoDismissMs?: number;
|
|
22
|
+
onDismiss: () => void;
|
|
23
|
+
itemImage?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ItemSoldCelebration: React.FC<ItemSoldCelebrationProps> = ({
|
|
27
|
+
visible,
|
|
28
|
+
item,
|
|
29
|
+
winnerName,
|
|
30
|
+
winnerProfilePic,
|
|
31
|
+
winningBid,
|
|
32
|
+
isMe,
|
|
33
|
+
marketType = 'FOR_MONEY',
|
|
34
|
+
nextItem,
|
|
35
|
+
nextItemDeadline,
|
|
36
|
+
autoDismissMs = 5000,
|
|
37
|
+
onDismiss,
|
|
38
|
+
itemImage,
|
|
39
|
+
}) => {
|
|
40
|
+
const { theme } = useTheme();
|
|
41
|
+
const opacityAnim = useRef(new Animated.Value(0)).current;
|
|
42
|
+
const scaleAnim = useRef(new Animated.Value(0.8)).current;
|
|
43
|
+
const trophyBounce = useRef(new Animated.Value(0)).current;
|
|
44
|
+
const confettiAnim = useRef(new Animated.Value(0)).current;
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!visible) {
|
|
48
|
+
Animated.timing(opacityAnim, { toValue: 0, duration: 300, useNativeDriver: true }).start();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Entrance
|
|
53
|
+
opacityAnim.setValue(0);
|
|
54
|
+
scaleAnim.setValue(0.8);
|
|
55
|
+
trophyBounce.setValue(0);
|
|
56
|
+
confettiAnim.setValue(0);
|
|
57
|
+
|
|
58
|
+
Animated.parallel([
|
|
59
|
+
Animated.timing(opacityAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
60
|
+
Animated.spring(scaleAnim, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }),
|
|
61
|
+
]).start(() => {
|
|
62
|
+
// Trophy bounce
|
|
63
|
+
Animated.sequence([
|
|
64
|
+
Animated.timing(trophyBounce, { toValue: -20, duration: 200, useNativeDriver: true }),
|
|
65
|
+
Animated.spring(trophyBounce, { toValue: 0, friction: 3, tension: 120, useNativeDriver: true }),
|
|
66
|
+
]).start();
|
|
67
|
+
|
|
68
|
+
// Confetti burst
|
|
69
|
+
Animated.timing(confettiAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Auto-dismiss
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
Animated.timing(opacityAnim, { toValue: 0, duration: 400, useNativeDriver: true }).start(() => onDismiss());
|
|
75
|
+
}, autoDismissMs);
|
|
76
|
+
|
|
77
|
+
return () => clearTimeout(timer);
|
|
78
|
+
}, [visible]);
|
|
79
|
+
|
|
80
|
+
if (!visible || !item) return null;
|
|
81
|
+
|
|
82
|
+
const accentColor = isMe ? '#10B981' : '#F59E0B';
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Animated.View style={[styles.overlay, { opacity: opacityAnim }]}>
|
|
86
|
+
<View variant="transparent" style={styles.backdrop} />
|
|
87
|
+
<Animated.View style={[styles.card, { transform: [{ scale: scaleAnim }] }]}>
|
|
88
|
+
{/* Confetti dots */}
|
|
89
|
+
{[...Array(12)].map((_, i) => {
|
|
90
|
+
const angle = (i / 12) * Math.PI * 2;
|
|
91
|
+
const colors = ['#F59E0B', '#10B981', '#3B82F6', '#EF4444', '#8B5CF6', '#EC4899'];
|
|
92
|
+
return (
|
|
93
|
+
<Animated.View
|
|
94
|
+
key={i}
|
|
95
|
+
style={{
|
|
96
|
+
...StyleSheet.absoluteFillObject,
|
|
97
|
+
top: '50%',
|
|
98
|
+
left: '50%',
|
|
99
|
+
width: 8,
|
|
100
|
+
height: 8,
|
|
101
|
+
borderRadius: 4,
|
|
102
|
+
backgroundColor: colors[i % colors.length],
|
|
103
|
+
opacity: confettiAnim,
|
|
104
|
+
transform: [
|
|
105
|
+
{ translateX: Animated.multiply(confettiAnim, Math.cos(angle) * 120) as any },
|
|
106
|
+
{ translateY: Animated.multiply(confettiAnim, Math.sin(angle) * 120 - 40) as any },
|
|
107
|
+
{ scale: Animated.subtract(1, Animated.multiply(confettiAnim, 0.5)) as any },
|
|
108
|
+
],
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
})}
|
|
113
|
+
|
|
114
|
+
{/* Trophy */}
|
|
115
|
+
<Animated.View style={{ transform: [{ translateY: trophyBounce }], marginBottom: 16 }}>
|
|
116
|
+
<View variant="transparent" style={[styles.trophyCircle, { backgroundColor: accentColor + '20' }]}>
|
|
117
|
+
<Ionicons name="trophy" size={40} color={accentColor} />
|
|
118
|
+
</View>
|
|
119
|
+
</Animated.View>
|
|
120
|
+
|
|
121
|
+
{/* SOLD label */}
|
|
122
|
+
<Text style={[styles.soldLabel, { color: accentColor }]}>SOLD!</Text>
|
|
123
|
+
|
|
124
|
+
{/* Item info */}
|
|
125
|
+
<View variant="transparent" style={styles.itemRow}>
|
|
126
|
+
{itemImage ? (
|
|
127
|
+
<Image source={{ uri: itemImage }} style={styles.itemImage} resizeMode="cover" />
|
|
128
|
+
) : (
|
|
129
|
+
<View variant="transparent" style={[styles.itemImage, { backgroundColor: theme.colors.surface.elevated, alignItems: 'center', justifyContent: 'center' }]}>
|
|
130
|
+
<Ionicons name="trophy-outline" size={20} color={theme.colors.text.tertiary} />
|
|
131
|
+
</View>
|
|
132
|
+
)}
|
|
133
|
+
<View variant="transparent" style={{ flex: 1, marginLeft: 12 }}>
|
|
134
|
+
<Text style={styles.itemName}>{item.item_name}</Text>
|
|
135
|
+
{item.seed != null && <Text style={styles.itemSeed}>Seed #{item.seed}</Text>}
|
|
136
|
+
</View>
|
|
137
|
+
</View>
|
|
138
|
+
|
|
139
|
+
{/* Winner + bid */}
|
|
140
|
+
<View variant="transparent" style={[styles.winnerRow, { backgroundColor: accentColor + '10', borderColor: accentColor + '30' }]}>
|
|
141
|
+
{winnerProfilePic ? (
|
|
142
|
+
<Image source={{ uri: winnerProfilePic }} style={styles.winnerAvatar} />
|
|
143
|
+
) : (
|
|
144
|
+
<View variant="transparent" style={[styles.winnerAvatar, { backgroundColor: accentColor + '30', alignItems: 'center', justifyContent: 'center' }]}>
|
|
145
|
+
<Text style={{ color: accentColor, fontWeight: '700', fontSize: 14, lineHeight: 19 }}>
|
|
146
|
+
{(winnerName || '?').charAt(0).toUpperCase()}
|
|
147
|
+
</Text>
|
|
148
|
+
</View>
|
|
149
|
+
)}
|
|
150
|
+
<View variant="transparent" style={{ flex: 1, marginLeft: 10 }}>
|
|
151
|
+
<Text style={[styles.winnerName, { color: accentColor }]}>
|
|
152
|
+
{isMe ? 'You won!' : winnerName || 'Winner'}
|
|
153
|
+
</Text>
|
|
154
|
+
<Text style={styles.bidAmount}>
|
|
155
|
+
{formatCurrency(winningBid ?? 0, marketType)}
|
|
156
|
+
</Text>
|
|
157
|
+
</View>
|
|
158
|
+
{isMe && <Ionicons name="checkmark-circle" size={28} color={accentColor} />}
|
|
159
|
+
</View>
|
|
160
|
+
|
|
161
|
+
{/* Next item teaser */}
|
|
162
|
+
{nextItem && (
|
|
163
|
+
<View variant="transparent" style={styles.nextSection}>
|
|
164
|
+
<View variant="transparent" style={styles.divider} />
|
|
165
|
+
<Text style={styles.nextLabel}>Next up</Text>
|
|
166
|
+
<Text style={styles.nextItemName}>{nextItem.item_name}</Text>
|
|
167
|
+
{nextItemDeadline && (
|
|
168
|
+
<View variant="transparent" style={{ marginTop: 6 }}>
|
|
169
|
+
<CalcuttaCountdown targetDate={nextItemDeadline} label="Bidding starts" size="compact" />
|
|
170
|
+
</View>
|
|
171
|
+
)}
|
|
172
|
+
</View>
|
|
173
|
+
)}
|
|
174
|
+
</Animated.View>
|
|
175
|
+
</Animated.View>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const styles = StyleSheet.create({
|
|
180
|
+
overlay: {
|
|
181
|
+
...StyleSheet.absoluteFillObject,
|
|
182
|
+
zIndex: 998,
|
|
183
|
+
alignItems: 'center',
|
|
184
|
+
justifyContent: 'center',
|
|
185
|
+
},
|
|
186
|
+
backdrop: {
|
|
187
|
+
...StyleSheet.absoluteFillObject,
|
|
188
|
+
backgroundColor: 'rgba(0,0,0,0.75)',
|
|
189
|
+
},
|
|
190
|
+
card: {
|
|
191
|
+
backgroundColor: '#1a1a2e',
|
|
192
|
+
borderRadius: 24,
|
|
193
|
+
padding: 28,
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
width: '85%',
|
|
196
|
+
maxWidth: 360,
|
|
197
|
+
overflow: 'visible',
|
|
198
|
+
},
|
|
199
|
+
trophyCircle: {
|
|
200
|
+
width: 80,
|
|
201
|
+
height: 80,
|
|
202
|
+
borderRadius: 40,
|
|
203
|
+
alignItems: 'center',
|
|
204
|
+
justifyContent: 'center',
|
|
205
|
+
},
|
|
206
|
+
soldLabel: {
|
|
207
|
+
fontSize: 28,
|
|
208
|
+
lineHeight: 37,
|
|
209
|
+
fontWeight: '900',
|
|
210
|
+
letterSpacing: 4,
|
|
211
|
+
marginBottom: 20,
|
|
212
|
+
},
|
|
213
|
+
itemRow: {
|
|
214
|
+
flexDirection: 'row',
|
|
215
|
+
alignItems: 'center',
|
|
216
|
+
width: '100%',
|
|
217
|
+
marginBottom: 16,
|
|
218
|
+
},
|
|
219
|
+
itemImage: {
|
|
220
|
+
width: 48,
|
|
221
|
+
height: 48,
|
|
222
|
+
borderRadius: 10,
|
|
223
|
+
},
|
|
224
|
+
itemName: {
|
|
225
|
+
color: '#FFFFFF',
|
|
226
|
+
fontSize: 18,
|
|
227
|
+
lineHeight: 24,
|
|
228
|
+
fontWeight: '700',
|
|
229
|
+
},
|
|
230
|
+
itemSeed: {
|
|
231
|
+
color: 'rgba(255,255,255,0.5)',
|
|
232
|
+
fontSize: 13,
|
|
233
|
+
lineHeight: 17,
|
|
234
|
+
marginTop: 2,
|
|
235
|
+
},
|
|
236
|
+
winnerRow: {
|
|
237
|
+
flexDirection: 'row',
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
width: '100%',
|
|
240
|
+
padding: 14,
|
|
241
|
+
borderRadius: 14,
|
|
242
|
+
borderWidth: 1,
|
|
243
|
+
},
|
|
244
|
+
winnerAvatar: {
|
|
245
|
+
width: 36,
|
|
246
|
+
height: 36,
|
|
247
|
+
borderRadius: 18,
|
|
248
|
+
overflow: 'hidden',
|
|
249
|
+
},
|
|
250
|
+
winnerName: {
|
|
251
|
+
fontSize: 15,
|
|
252
|
+
lineHeight: 20,
|
|
253
|
+
fontWeight: '700',
|
|
254
|
+
},
|
|
255
|
+
bidAmount: {
|
|
256
|
+
color: '#FFFFFF',
|
|
257
|
+
fontSize: 18,
|
|
258
|
+
lineHeight: 24,
|
|
259
|
+
fontWeight: '800',
|
|
260
|
+
marginTop: 2,
|
|
261
|
+
},
|
|
262
|
+
nextSection: {
|
|
263
|
+
width: '100%',
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
marginTop: 16,
|
|
266
|
+
},
|
|
267
|
+
divider: {
|
|
268
|
+
width: 40,
|
|
269
|
+
height: 2,
|
|
270
|
+
backgroundColor: 'rgba(255,255,255,0.15)',
|
|
271
|
+
borderRadius: 1,
|
|
272
|
+
marginBottom: 12,
|
|
273
|
+
},
|
|
274
|
+
nextLabel: {
|
|
275
|
+
color: 'rgba(255,255,255,0.4)',
|
|
276
|
+
fontSize: 12,
|
|
277
|
+
lineHeight: 16,
|
|
278
|
+
textTransform: 'uppercase',
|
|
279
|
+
letterSpacing: 1,
|
|
280
|
+
},
|
|
281
|
+
nextItemName: {
|
|
282
|
+
color: '#FFFFFF',
|
|
283
|
+
fontSize: 16,
|
|
284
|
+
lineHeight: 21,
|
|
285
|
+
fontWeight: '600',
|
|
286
|
+
marginTop: 4,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
@@ -26,7 +26,9 @@ import { CalcuttaLeaderboard } from '../CalcuttaLeaderboard';
|
|
|
26
26
|
interface SealedBidAuctionProps {
|
|
27
27
|
calcutta_competition_id: string;
|
|
28
28
|
player_id?: string;
|
|
29
|
-
onClose
|
|
29
|
+
onClose?: () => void;
|
|
30
|
+
access_token?: string;
|
|
31
|
+
device_id?: string;
|
|
30
32
|
getSocket?: () => WebSocket | null;
|
|
31
33
|
getAccessToken?: () => string | undefined;
|
|
32
34
|
getDeviceId?: () => string | undefined;
|
|
@@ -53,9 +55,8 @@ export const SealedBidAuction: React.FC<SealedBidAuctionProps> = ({
|
|
|
53
55
|
calcutta_competition_id,
|
|
54
56
|
player_id,
|
|
55
57
|
onClose,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
getDeviceId,
|
|
58
|
+
access_token,
|
|
59
|
+
device_id,
|
|
59
60
|
player_data,
|
|
60
61
|
player_balance,
|
|
61
62
|
onDepositFunds,
|
|
@@ -145,14 +146,11 @@ export const SealedBidAuction: React.FC<SealedBidAuctionProps> = ({
|
|
|
145
146
|
const { images: itemImages } = useCalcuttaItemImages(items);
|
|
146
147
|
|
|
147
148
|
// Socket for presence
|
|
148
|
-
const noopSocket = useCallback(() => null, []);
|
|
149
|
-
const noopString = useCallback(() => undefined, []);
|
|
150
149
|
const { connected: socketConnected, presence } = useCalcuttaSocket(
|
|
151
150
|
calcutta_competition_id,
|
|
151
|
+
access_token,
|
|
152
|
+
device_id,
|
|
152
153
|
player_data || { username: '' },
|
|
153
|
-
getSocket || noopSocket,
|
|
154
|
-
getAccessToken || noopString,
|
|
155
|
-
getDeviceId || noopString,
|
|
156
154
|
);
|
|
157
155
|
|
|
158
156
|
// Adaptive polling based on lifecycle state
|