@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bettoredge/calcutta",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Calcutta auction competition components for BettorEdge applications",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -41,6 +41,7 @@
41
41
  "react-native": ">=0.76.5",
42
42
  "@expo/vector-icons": "*",
43
43
  "@bettoredge/api": ">=0.8.3",
44
- "@bettoredge/types": ">=0.5.2"
44
+ "@bettoredge/types": ">=0.5.2",
45
+ "@bettoredge/socket": ">=0.1.1"
45
46
  }
46
47
  }
@@ -0,0 +1,306 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { StyleSheet, Animated, ScrollView, Image, TouchableOpacity } 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
+
8
+ interface PlayerSummary {
9
+ player_id: string;
10
+ username?: string;
11
+ profile_pic?: string;
12
+ items_won: number;
13
+ total_spent: number;
14
+ is_me?: boolean;
15
+ }
16
+
17
+ export interface AuctionCompleteOverlayProps {
18
+ visible: boolean;
19
+ competitionName?: string;
20
+ totalPot: number;
21
+ marketType: string;
22
+ myItemsWon: CalcuttaAuctionItemProps[];
23
+ myTotalSpent: number;
24
+ leaderboard: PlayerSummary[];
25
+ onDismiss: () => void;
26
+ }
27
+
28
+ export const AuctionCompleteOverlay: React.FC<AuctionCompleteOverlayProps> = ({
29
+ visible,
30
+ competitionName,
31
+ totalPot,
32
+ marketType,
33
+ myItemsWon,
34
+ myTotalSpent,
35
+ leaderboard,
36
+ onDismiss,
37
+ }) => {
38
+ const { theme } = useTheme();
39
+ const opacityAnim = useRef(new Animated.Value(0)).current;
40
+ const slideAnim = useRef(new Animated.Value(40)).current;
41
+
42
+ useEffect(() => {
43
+ if (visible) {
44
+ opacityAnim.setValue(0);
45
+ slideAnim.setValue(40);
46
+ Animated.parallel([
47
+ Animated.timing(opacityAnim, { toValue: 1, duration: 400, useNativeDriver: true }),
48
+ Animated.spring(slideAnim, { toValue: 0, friction: 6, tension: 60, useNativeDriver: true }),
49
+ ]).start();
50
+ } else {
51
+ Animated.timing(opacityAnim, { toValue: 0, duration: 300, useNativeDriver: true }).start();
52
+ }
53
+ }, [visible]);
54
+
55
+ if (!visible) return null;
56
+
57
+ const medals = ['🥇', '🥈', '🥉'];
58
+
59
+ return (
60
+ <Animated.View style={[styles.overlay, { opacity: opacityAnim }]}>
61
+ <View variant="transparent" style={styles.backdrop} />
62
+ <Animated.View style={[styles.content, { transform: [{ translateY: slideAnim }] }]}>
63
+ <ScrollView
64
+ showsVerticalScrollIndicator={false}
65
+ contentContainerStyle={styles.scrollContent}
66
+ >
67
+ {/* Header */}
68
+ <Ionicons name="trophy" size={48} color="#F59E0B" />
69
+ <Text style={styles.title}>Auction Complete!</Text>
70
+ <Text style={styles.compName}>{competitionName}</Text>
71
+
72
+ {/* Total pot */}
73
+ <View variant="transparent" style={styles.potCard}>
74
+ <Text style={styles.potLabel}>Total Pot</Text>
75
+ <Text style={styles.potAmount}>{formatCurrency(totalPot, marketType)}</Text>
76
+ </View>
77
+
78
+ {/* My results */}
79
+ {myItemsWon.length > 0 ? (
80
+ <View variant="transparent" style={styles.section}>
81
+ <Text style={styles.sectionTitle}>Your Items ({myItemsWon.length})</Text>
82
+ {myItemsWon.map(item => (
83
+ <View key={item.calcutta_auction_item_id} variant="transparent" style={styles.itemRow}>
84
+ <Ionicons name="checkmark-circle" size={16} color="#10B981" />
85
+ <Text style={styles.itemName} numberOfLines={1}>{item.item_name}</Text>
86
+ <Text style={styles.itemBid}>{formatCurrency(item.winning_bid, marketType)}</Text>
87
+ </View>
88
+ ))}
89
+ <View variant="transparent" style={styles.totalRow}>
90
+ <Text style={styles.totalLabel}>Total Spent</Text>
91
+ <Text style={styles.totalAmount}>{formatCurrency(myTotalSpent, marketType)}</Text>
92
+ </View>
93
+ </View>
94
+ ) : (
95
+ <View variant="transparent" style={styles.section}>
96
+ <Text style={styles.noItems}>You didn't win any items this auction</Text>
97
+ </View>
98
+ )}
99
+
100
+ {/* Leaderboard */}
101
+ {leaderboard.length > 0 && (
102
+ <View variant="transparent" style={styles.section}>
103
+ <Text style={styles.sectionTitle}>Leaderboard</Text>
104
+ {leaderboard.map((player, i) => (
105
+ <View key={player.player_id} variant="transparent" style={styles.leaderRow}>
106
+ <Text style={styles.rank}>{i < 3 ? medals[i] : `${i + 1}`}</Text>
107
+ {player.profile_pic ? (
108
+ <Image source={{ uri: player.profile_pic }} style={styles.avatar} />
109
+ ) : (
110
+ <View variant="transparent" style={[styles.avatar, { backgroundColor: player.is_me ? '#10B98130' : 'rgba(255,255,255,0.15)', alignItems: 'center', justifyContent: 'center' }]}>
111
+ <Text style={{ color: player.is_me ? '#10B981' : '#FFFFFF', fontSize: 11, lineHeight: 15, fontWeight: '700' }}>
112
+ {(player.username || '?').charAt(0).toUpperCase()}
113
+ </Text>
114
+ </View>
115
+ )}
116
+ <View variant="transparent" style={{ flex: 1, marginLeft: 8 }}>
117
+ <Text style={[styles.leaderName, player.is_me && { color: '#10B981' }]}>
118
+ {player.is_me ? 'You' : player.username || 'Player'}
119
+ </Text>
120
+ <Text style={styles.leaderSub}>
121
+ {player.items_won} item{player.items_won !== 1 ? 's' : ''}
122
+ </Text>
123
+ </View>
124
+ <Text style={styles.leaderSpent}>{formatCurrency(player.total_spent, marketType)}</Text>
125
+ </View>
126
+ ))}
127
+ </View>
128
+ )}
129
+
130
+ {/* Dismiss */}
131
+ <TouchableOpacity style={styles.dismissBtn} onPress={onDismiss} activeOpacity={0.7}>
132
+ <Text style={styles.dismissText}>Continue</Text>
133
+ </TouchableOpacity>
134
+ </ScrollView>
135
+ </Animated.View>
136
+ </Animated.View>
137
+ );
138
+ };
139
+
140
+ const styles = StyleSheet.create({
141
+ overlay: {
142
+ ...StyleSheet.absoluteFillObject,
143
+ zIndex: 997,
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ },
147
+ backdrop: {
148
+ ...StyleSheet.absoluteFillObject,
149
+ backgroundColor: 'rgba(0,0,0,0.85)',
150
+ },
151
+ content: {
152
+ width: '90%',
153
+ maxWidth: 400,
154
+ maxHeight: '85%',
155
+ backgroundColor: '#1a1a2e',
156
+ borderRadius: 24,
157
+ overflow: 'hidden',
158
+ },
159
+ scrollContent: {
160
+ padding: 28,
161
+ alignItems: 'center',
162
+ },
163
+ title: {
164
+ color: '#FFFFFF',
165
+ fontSize: 26,
166
+ lineHeight: 34,
167
+ fontWeight: '900',
168
+ marginTop: 12,
169
+ },
170
+ compName: {
171
+ color: 'rgba(255,255,255,0.5)',
172
+ fontSize: 14,
173
+ lineHeight: 19,
174
+ marginTop: 4,
175
+ marginBottom: 20,
176
+ },
177
+ potCard: {
178
+ backgroundColor: 'rgba(245,158,11,0.15)',
179
+ borderRadius: 14,
180
+ paddingVertical: 16,
181
+ paddingHorizontal: 24,
182
+ alignItems: 'center',
183
+ width: '100%',
184
+ marginBottom: 20,
185
+ },
186
+ potLabel: {
187
+ color: '#F59E0B',
188
+ fontSize: 12,
189
+ lineHeight: 16,
190
+ textTransform: 'uppercase',
191
+ letterSpacing: 1,
192
+ },
193
+ potAmount: {
194
+ color: '#F59E0B',
195
+ fontSize: 32,
196
+ lineHeight: 42,
197
+ fontWeight: '900',
198
+ marginTop: 4,
199
+ },
200
+ section: {
201
+ width: '100%',
202
+ marginBottom: 16,
203
+ },
204
+ sectionTitle: {
205
+ color: 'rgba(255,255,255,0.5)',
206
+ fontSize: 11,
207
+ lineHeight: 15,
208
+ textTransform: 'uppercase',
209
+ letterSpacing: 1,
210
+ marginBottom: 10,
211
+ },
212
+ itemRow: {
213
+ flexDirection: 'row',
214
+ alignItems: 'center',
215
+ paddingVertical: 8,
216
+ borderBottomWidth: 1,
217
+ borderBottomColor: 'rgba(255,255,255,0.08)',
218
+ },
219
+ itemName: {
220
+ color: '#FFFFFF',
221
+ fontSize: 14,
222
+ lineHeight: 19,
223
+ flex: 1,
224
+ marginLeft: 8,
225
+ },
226
+ itemBid: {
227
+ color: 'rgba(255,255,255,0.7)',
228
+ fontSize: 14,
229
+ lineHeight: 19,
230
+ fontWeight: '600',
231
+ },
232
+ totalRow: {
233
+ flexDirection: 'row',
234
+ justifyContent: 'space-between',
235
+ paddingTop: 10,
236
+ },
237
+ totalLabel: {
238
+ color: 'rgba(255,255,255,0.5)',
239
+ fontSize: 13,
240
+ lineHeight: 17,
241
+ },
242
+ totalAmount: {
243
+ color: '#FFFFFF',
244
+ fontSize: 16,
245
+ lineHeight: 21,
246
+ fontWeight: '800',
247
+ },
248
+ noItems: {
249
+ color: 'rgba(255,255,255,0.4)',
250
+ fontSize: 14,
251
+ lineHeight: 19,
252
+ textAlign: 'center',
253
+ paddingVertical: 12,
254
+ },
255
+ leaderRow: {
256
+ flexDirection: 'row',
257
+ alignItems: 'center',
258
+ paddingVertical: 8,
259
+ borderBottomWidth: 1,
260
+ borderBottomColor: 'rgba(255,255,255,0.08)',
261
+ },
262
+ rank: {
263
+ color: '#FFFFFF',
264
+ fontSize: 16,
265
+ lineHeight: 21,
266
+ width: 28,
267
+ textAlign: 'center',
268
+ },
269
+ avatar: {
270
+ width: 28,
271
+ height: 28,
272
+ borderRadius: 14,
273
+ },
274
+ leaderName: {
275
+ color: '#FFFFFF',
276
+ fontSize: 14,
277
+ lineHeight: 19,
278
+ fontWeight: '600',
279
+ },
280
+ leaderSub: {
281
+ color: 'rgba(255,255,255,0.4)',
282
+ fontSize: 11,
283
+ lineHeight: 15,
284
+ },
285
+ leaderSpent: {
286
+ color: 'rgba(255,255,255,0.7)',
287
+ fontSize: 14,
288
+ lineHeight: 19,
289
+ fontWeight: '600',
290
+ },
291
+ dismissBtn: {
292
+ backgroundColor: '#10B981',
293
+ height: 48,
294
+ borderRadius: 12,
295
+ alignItems: 'center',
296
+ justifyContent: 'center',
297
+ width: '100%',
298
+ marginTop: 8,
299
+ },
300
+ dismissText: {
301
+ color: '#FFFFFF',
302
+ fontSize: 16,
303
+ lineHeight: 21,
304
+ fontWeight: '700',
305
+ },
306
+ });
@@ -0,0 +1,178 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { StyleSheet, Animated, Dimensions } 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
+
7
+ export interface AuctionCountdownOverlayProps {
8
+ visible: boolean;
9
+ competitionName?: string;
10
+ firstItem?: CalcuttaAuctionItemProps;
11
+ countdownFrom?: number;
12
+ onComplete: () => void;
13
+ }
14
+
15
+ export const AuctionCountdownOverlay: React.FC<AuctionCountdownOverlayProps> = ({
16
+ visible,
17
+ competitionName,
18
+ firstItem,
19
+ countdownFrom = 5,
20
+ onComplete,
21
+ }) => {
22
+ const { theme } = useTheme();
23
+ const [count, setCount] = useState(countdownFrom);
24
+ const scaleAnim = useRef(new Animated.Value(1)).current;
25
+ const opacityAnim = useRef(new Animated.Value(0)).current;
26
+ const numberScale = useRef(new Animated.Value(0.5)).current;
27
+
28
+ useEffect(() => {
29
+ if (!visible) {
30
+ setCount(countdownFrom);
31
+ opacityAnim.setValue(0);
32
+ return;
33
+ }
34
+
35
+ // Fade in
36
+ Animated.timing(opacityAnim, {
37
+ toValue: 1,
38
+ duration: 300,
39
+ useNativeDriver: true,
40
+ }).start();
41
+
42
+ setCount(countdownFrom);
43
+ }, [visible]);
44
+
45
+ // Countdown tick
46
+ useEffect(() => {
47
+ if (!visible) return;
48
+ if (count <= 0) {
49
+ // Fade out then complete
50
+ Animated.timing(opacityAnim, {
51
+ toValue: 0,
52
+ duration: 400,
53
+ useNativeDriver: true,
54
+ }).start(() => onComplete());
55
+ return;
56
+ }
57
+
58
+ // Pop animation for each number
59
+ numberScale.setValue(0.3);
60
+ Animated.spring(numberScale, {
61
+ toValue: 1,
62
+ friction: 4,
63
+ tension: 100,
64
+ useNativeDriver: true,
65
+ }).start();
66
+
67
+ const timer = setTimeout(() => setCount(c => c - 1), 1000);
68
+ return () => clearTimeout(timer);
69
+ }, [count, visible]);
70
+
71
+ if (!visible) return null;
72
+
73
+
74
+ return (
75
+ <Animated.View style={[styles.overlay, { opacity: opacityAnim }]}>
76
+ <View variant="transparent" style={styles.backdrop} />
77
+ <View variant="transparent" style={styles.content}>
78
+ {/* Competition name */}
79
+ <Text variant="caption" bold style={styles.label}>
80
+ {competitionName || 'Auction'}
81
+ </Text>
82
+
83
+ {/* Status text */}
84
+ <Text variant="body" style={styles.subtitle}>
85
+ {count > 0 ? 'Starting in' : 'GO!'}
86
+ </Text>
87
+
88
+ {/* Big countdown number */}
89
+ {count > 0 && (
90
+ <Animated.View style={{ transform: [{ scale: numberScale }] }}>
91
+ <Text style={[styles.countNumber, { color: count <= 3 ? '#EF4444' : '#FFFFFF' }]}>
92
+ {count}
93
+ </Text>
94
+ </Animated.View>
95
+ )}
96
+
97
+ {count <= 0 && (
98
+ <Animated.View style={{ transform: [{ scale: numberScale }] }}>
99
+ <Ionicons name="flash" size={80} color="#F59E0B" />
100
+ </Animated.View>
101
+ )}
102
+
103
+ {/* First item preview */}
104
+ {firstItem && (
105
+ <View variant="transparent" style={styles.itemPreview}>
106
+ <Text variant="caption" style={styles.itemLabel}>First up</Text>
107
+ <Text variant="h3" bold style={styles.itemName}>{firstItem.item_name}</Text>
108
+ {firstItem.seed != null && (
109
+ <Text variant="caption" style={styles.itemSeed}>Seed #{firstItem.seed}</Text>
110
+ )}
111
+ </View>
112
+ )}
113
+ </View>
114
+ </Animated.View>
115
+ );
116
+ };
117
+
118
+ const styles = StyleSheet.create({
119
+ overlay: {
120
+ ...StyleSheet.absoluteFillObject,
121
+ zIndex: 999,
122
+ alignItems: 'center',
123
+ justifyContent: 'center',
124
+ },
125
+ backdrop: {
126
+ ...StyleSheet.absoluteFillObject,
127
+ backgroundColor: 'rgba(0,0,0,0.85)',
128
+ },
129
+ content: {
130
+ alignItems: 'center',
131
+ justifyContent: 'center',
132
+ padding: 40,
133
+ },
134
+ label: {
135
+ color: 'rgba(255,255,255,0.6)',
136
+ fontSize: 14,
137
+ lineHeight: 19,
138
+ letterSpacing: 2,
139
+ textTransform: 'uppercase',
140
+ marginBottom: 8,
141
+ },
142
+ subtitle: {
143
+ color: 'rgba(255,255,255,0.8)',
144
+ fontSize: 18,
145
+ lineHeight: 24,
146
+ marginBottom: 16,
147
+ },
148
+ countNumber: {
149
+ fontSize: 120,
150
+ fontWeight: '900',
151
+ lineHeight: 156,
152
+ },
153
+ itemPreview: {
154
+ marginTop: 40,
155
+ alignItems: 'center',
156
+ backgroundColor: 'rgba(255,255,255,0.1)',
157
+ paddingHorizontal: 30,
158
+ paddingVertical: 16,
159
+ borderRadius: 16,
160
+ },
161
+ itemLabel: {
162
+ color: 'rgba(255,255,255,0.5)',
163
+ fontSize: 12,
164
+ lineHeight: 16,
165
+ textTransform: 'uppercase',
166
+ letterSpacing: 1,
167
+ },
168
+ itemName: {
169
+ color: '#FFFFFF',
170
+ fontSize: 22,
171
+ lineHeight: 29,
172
+ marginTop: 4,
173
+ },
174
+ itemSeed: {
175
+ color: 'rgba(255,255,255,0.5)',
176
+ marginTop: 2,
177
+ },
178
+ });
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { StyleSheet, ScrollView } from 'react-native';
3
+ import { View, Text, useTheme } from '@bettoredge/styles';
4
+ import { Ionicons } from '@expo/vector-icons';
5
+ import type { CalcuttaCompetitionProps } from '@bettoredge/types';
6
+ import { formatCurrency } from '../helpers/formatting';
7
+
8
+ export interface AuctionInfoChipsProps {
9
+ competition: CalcuttaCompetitionProps;
10
+ }
11
+
12
+ interface ChipData {
13
+ icon: string;
14
+ text: string;
15
+ }
16
+
17
+ const getChips = (comp: CalcuttaCompetitionProps): ChipData[] => {
18
+ const chips: ChipData[] = [];
19
+ const type = comp.auction_type;
20
+
21
+ if (type === 'live') {
22
+ chips.push({ icon: 'flash-outline', text: 'Live Auction' });
23
+ if (comp.item_timer_seconds) {
24
+ chips.push({ icon: 'timer-outline', text: `${comp.item_timer_seconds}s per item` });
25
+ }
26
+ if (comp.bid_extension_seconds) {
27
+ chips.push({ icon: 'expand-outline', text: `${comp.bid_extension_seconds}s extension` });
28
+ }
29
+ } else if (type === 'sealed_bid') {
30
+ chips.push({ icon: 'lock-closed-outline', text: 'Sealed Bids' });
31
+ } else if (type === 'sweepstakes') {
32
+ chips.push({ icon: 'shuffle-outline', text: 'Random Draw' });
33
+ }
34
+
35
+ if (type !== 'sweepstakes') {
36
+ if (comp.min_bid && Number(comp.min_bid) > 0) {
37
+ chips.push({ icon: 'cash-outline', text: `${formatCurrency(Number(comp.min_bid), comp.market_type)} min bid` });
38
+ }
39
+ if (comp.bid_increment && Number(comp.bid_increment) > 0) {
40
+ chips.push({ icon: 'trending-up-outline', text: `${formatCurrency(Number(comp.bid_increment), comp.market_type)} increments` });
41
+ }
42
+ if (comp.max_escrow && Number(comp.max_escrow) > 0) {
43
+ chips.push({ icon: 'wallet-outline', text: `${formatCurrency(Number(comp.max_escrow), comp.market_type)} budget cap` });
44
+ }
45
+ }
46
+
47
+ if (comp.max_participants && Number(comp.max_participants) > 0) {
48
+ chips.push({ icon: 'people-outline', text: `${comp.max_participants} spots` });
49
+ }
50
+
51
+ const entryFee = Number(comp.entry_fee) || 0;
52
+ if (entryFee === 0) {
53
+ chips.push({ icon: 'gift-outline', text: 'Free entry' });
54
+ }
55
+
56
+ return chips;
57
+ };
58
+
59
+ export const AuctionInfoChips: React.FC<AuctionInfoChipsProps> = ({ competition }) => {
60
+ const { theme } = useTheme();
61
+ const chips = getChips(competition);
62
+
63
+ if (chips.length === 0) return null;
64
+
65
+ return (
66
+ <ScrollView
67
+ horizontal
68
+ showsHorizontalScrollIndicator={false}
69
+ style={styles.scroll}
70
+ contentContainerStyle={styles.scrollContent}
71
+ >
72
+ {chips.map((chip, i) => (
73
+ <View
74
+ key={i}
75
+ variant="transparent"
76
+ style={[styles.chip, { backgroundColor: theme.colors.surface.elevated, borderColor: theme.colors.border.subtle }]}
77
+ >
78
+ <Ionicons name={chip.icon as any} size={13} color={theme.colors.text.secondary} />
79
+ <Text variant="caption" color="secondary" style={styles.chipText}>{chip.text}</Text>
80
+ </View>
81
+ ))}
82
+ </ScrollView>
83
+ );
84
+ };
85
+
86
+ const styles = StyleSheet.create({
87
+ scroll: {
88
+ marginTop: 10,
89
+ },
90
+ scrollContent: {
91
+ gap: 6,
92
+ flexDirection: 'row',
93
+ },
94
+ chip: {
95
+ flexDirection: 'row',
96
+ alignItems: 'center',
97
+ paddingHorizontal: 10,
98
+ paddingVertical: 5,
99
+ borderRadius: 20,
100
+ borderWidth: 1,
101
+ },
102
+ chipText: {
103
+ marginLeft: 4,
104
+ },
105
+ });