@bettoredge/calcutta 0.4.0 → 0.4.2
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 +4 -4
- package/src/components/CalcuttaActionCard.tsx +31 -2
- package/src/components/CalcuttaAuction.tsx +0 -7
- package/src/components/CalcuttaDetail.tsx +247 -168
- package/src/components/CalcuttaResults.tsx +461 -0
- package/src/components/CalcuttaWalkthrough.tsx +371 -0
- package/src/components/sealed/SealedBidAuction.tsx +140 -158
- package/src/index.ts +4 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useMemo, useState } from 'react';
|
|
2
|
+
import { StyleSheet, Animated, TouchableOpacity, ScrollView, useWindowDimensions, NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
|
|
3
|
+
import { View, Text, useTheme } from '@bettoredge/styles';
|
|
4
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
5
|
+
|
|
6
|
+
export interface CalcuttaWalkthroughProps {
|
|
7
|
+
visible: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
auction_type: 'live' | 'sealed_bid' | 'sweepstakes';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SlideData {
|
|
13
|
+
icon: string;
|
|
14
|
+
iconColor: string;
|
|
15
|
+
title: string;
|
|
16
|
+
body: string;
|
|
17
|
+
bullets?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SLIDE_WHAT: SlideData = {
|
|
21
|
+
icon: 'trophy-outline',
|
|
22
|
+
iconColor: '#F59E0B',
|
|
23
|
+
title: 'What is a Calcutta?',
|
|
24
|
+
body: 'A Calcutta is a group auction pool. Players bid on teams or athletes in an auction. All winning bids form the prize pot. As your teams advance through tournament rounds, you earn payouts from the pot.',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const SLIDE_AUCTION: Record<string, SlideData> = {
|
|
28
|
+
live: {
|
|
29
|
+
icon: 'flash-outline',
|
|
30
|
+
iconColor: '#3B82F6',
|
|
31
|
+
title: 'Live Auction',
|
|
32
|
+
body: 'Items are auctioned one at a time with a countdown timer. Place your bid before time runs out \u2014 highest bid wins!',
|
|
33
|
+
bullets: [
|
|
34
|
+
'Each item has its own timer',
|
|
35
|
+
'Timer extends if a bid comes in near the end',
|
|
36
|
+
'Stay sharp and watch for outbid alerts',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
sealed_bid: {
|
|
40
|
+
icon: 'lock-closed-outline',
|
|
41
|
+
iconColor: '#8B5CF6',
|
|
42
|
+
title: 'Sealed Bid Auction',
|
|
43
|
+
body: 'Place hidden bids on any item before the deadline. No one sees your bid until bidding closes.',
|
|
44
|
+
bullets: [
|
|
45
|
+
'Bid on as many items as you want',
|
|
46
|
+
'Highest bid on each item wins',
|
|
47
|
+
'Results revealed when the auction ends',
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
sweepstakes: {
|
|
51
|
+
icon: 'shuffle-outline',
|
|
52
|
+
iconColor: '#10B981',
|
|
53
|
+
title: 'Sweepstakes Draw',
|
|
54
|
+
body: 'No bidding required! When the host starts the competition, teams are randomly assigned to players.',
|
|
55
|
+
bullets: [
|
|
56
|
+
'Just join and wait for the draw',
|
|
57
|
+
'Every player gets a team',
|
|
58
|
+
'Root for your team to advance!',
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const SLIDE_ESCROW: SlideData = {
|
|
64
|
+
icon: 'wallet-outline',
|
|
65
|
+
iconColor: '#10B981',
|
|
66
|
+
title: 'Escrow & Funding',
|
|
67
|
+
body: 'Before bidding, deposit funds into your escrow balance. This is your bidding budget so you don\u2019t hit your wallet on every bid.',
|
|
68
|
+
bullets: [
|
|
69
|
+
'When you bid, funds move from \u201Cavailable\u201D to \u201Ccommitted\u201D',
|
|
70
|
+
'If you\u2019re outbid, funds return to available',
|
|
71
|
+
'After the auction, unused escrow is returned to your wallet',
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const SLIDE_PAYOUT: SlideData = {
|
|
76
|
+
icon: 'cash-outline',
|
|
77
|
+
iconColor: '#F59E0B',
|
|
78
|
+
title: 'How You Get Paid',
|
|
79
|
+
body: 'All winning bids form the prize pot. Each round of the tournament has a percentage of the pot allocated as payouts.',
|
|
80
|
+
bullets: [
|
|
81
|
+
'Teams that advance earn you payouts each round',
|
|
82
|
+
'Eliminated teams\u2019 share goes to advancing owners',
|
|
83
|
+
'Final round pays the biggest percentage',
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const CalcuttaWalkthrough: React.FC<CalcuttaWalkthroughProps> = ({
|
|
88
|
+
visible,
|
|
89
|
+
onClose,
|
|
90
|
+
auction_type,
|
|
91
|
+
}) => {
|
|
92
|
+
const { theme } = useTheme();
|
|
93
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
94
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
95
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
96
|
+
|
|
97
|
+
// Animations
|
|
98
|
+
const opacityAnim = useRef(new Animated.Value(0)).current;
|
|
99
|
+
const scaleAnim = useRef(new Animated.Value(0.85)).current;
|
|
100
|
+
const iconScales = useRef([
|
|
101
|
+
new Animated.Value(0),
|
|
102
|
+
new Animated.Value(0),
|
|
103
|
+
new Animated.Value(0),
|
|
104
|
+
new Animated.Value(0),
|
|
105
|
+
]).current;
|
|
106
|
+
|
|
107
|
+
const slides = useMemo(() => {
|
|
108
|
+
const s: SlideData[] = [SLIDE_WHAT];
|
|
109
|
+
s.push(SLIDE_AUCTION[auction_type] || SLIDE_AUCTION.live);
|
|
110
|
+
if (auction_type !== 'sweepstakes') {
|
|
111
|
+
s.push(SLIDE_ESCROW);
|
|
112
|
+
}
|
|
113
|
+
s.push(SLIDE_PAYOUT);
|
|
114
|
+
return s;
|
|
115
|
+
}, [auction_type]);
|
|
116
|
+
|
|
117
|
+
const cardWidth = Math.min(windowWidth * 0.9, 420);
|
|
118
|
+
const slideContentWidth = cardWidth - 56; // card padding 28 * 2
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!visible) {
|
|
122
|
+
Animated.timing(opacityAnim, { toValue: 0, duration: 250, useNativeDriver: true }).start();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setCurrentSlide(0);
|
|
127
|
+
scrollRef.current?.scrollTo({ x: 0, animated: false });
|
|
128
|
+
|
|
129
|
+
opacityAnim.setValue(0);
|
|
130
|
+
scaleAnim.setValue(0.85);
|
|
131
|
+
iconScales.forEach(s => s.setValue(0));
|
|
132
|
+
|
|
133
|
+
Animated.parallel([
|
|
134
|
+
Animated.timing(opacityAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
135
|
+
Animated.spring(scaleAnim, { toValue: 1, friction: 6, tension: 80, useNativeDriver: true }),
|
|
136
|
+
]).start(() => {
|
|
137
|
+
animateIcon(0);
|
|
138
|
+
});
|
|
139
|
+
}, [visible]);
|
|
140
|
+
|
|
141
|
+
const animateIcon = (index: number) => {
|
|
142
|
+
iconScales[index]?.setValue(0);
|
|
143
|
+
Animated.spring(iconScales[index], {
|
|
144
|
+
toValue: 1,
|
|
145
|
+
friction: 4,
|
|
146
|
+
tension: 100,
|
|
147
|
+
useNativeDriver: true,
|
|
148
|
+
}).start();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const goToSlide = (index: number) => {
|
|
152
|
+
scrollRef.current?.scrollTo({ x: index * slideContentWidth, animated: true });
|
|
153
|
+
setCurrentSlide(index);
|
|
154
|
+
animateIcon(index);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleNext = () => {
|
|
158
|
+
if (currentSlide < slides.length - 1) {
|
|
159
|
+
goToSlide(currentSlide + 1);
|
|
160
|
+
} else {
|
|
161
|
+
onClose();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const handleScrollEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
166
|
+
const newIndex = Math.round(e.nativeEvent.contentOffset.x / slideContentWidth);
|
|
167
|
+
if (newIndex !== currentSlide && newIndex >= 0 && newIndex < slides.length) {
|
|
168
|
+
setCurrentSlide(newIndex);
|
|
169
|
+
animateIcon(newIndex);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (!visible) return null;
|
|
174
|
+
|
|
175
|
+
const isLastSlide = currentSlide === slides.length - 1;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Animated.View style={[styles.overlay, { opacity: opacityAnim }]}>
|
|
179
|
+
<View variant="transparent" style={styles.backdrop} />
|
|
180
|
+
<Animated.View style={[styles.card, { width: cardWidth, transform: [{ scale: scaleAnim }] }]}>
|
|
181
|
+
|
|
182
|
+
{/* Slide content */}
|
|
183
|
+
<ScrollView
|
|
184
|
+
ref={scrollRef}
|
|
185
|
+
horizontal
|
|
186
|
+
pagingEnabled
|
|
187
|
+
showsHorizontalScrollIndicator={false}
|
|
188
|
+
scrollEventThrottle={16}
|
|
189
|
+
onMomentumScrollEnd={handleScrollEnd}
|
|
190
|
+
style={{ width: slideContentWidth }}
|
|
191
|
+
contentContainerStyle={{ width: slideContentWidth * slides.length }}
|
|
192
|
+
>
|
|
193
|
+
{slides.map((slide, i) => (
|
|
194
|
+
<View key={i} variant="transparent" style={[styles.slide, { width: slideContentWidth }]}>
|
|
195
|
+
{/* Icon circle */}
|
|
196
|
+
<Animated.View style={{ transform: [{ scale: iconScales[i] || new Animated.Value(1) }] }}>
|
|
197
|
+
<View variant="transparent" style={[styles.iconCircle, { backgroundColor: slide.iconColor + '20' }]}>
|
|
198
|
+
<Ionicons name={slide.icon as any} size={40} color={slide.iconColor} />
|
|
199
|
+
</View>
|
|
200
|
+
</Animated.View>
|
|
201
|
+
|
|
202
|
+
{/* Title */}
|
|
203
|
+
<Text style={styles.title}>{slide.title}</Text>
|
|
204
|
+
|
|
205
|
+
{/* Body */}
|
|
206
|
+
<Text style={styles.body}>{slide.body}</Text>
|
|
207
|
+
|
|
208
|
+
{/* Bullets */}
|
|
209
|
+
{slide.bullets?.map((bullet, j) => (
|
|
210
|
+
<View key={j} variant="transparent" style={styles.bulletRow}>
|
|
211
|
+
<View variant="transparent" style={[styles.bulletDot, { backgroundColor: slide.iconColor }]} />
|
|
212
|
+
<Text style={styles.bulletText}>{bullet}</Text>
|
|
213
|
+
</View>
|
|
214
|
+
))}
|
|
215
|
+
</View>
|
|
216
|
+
))}
|
|
217
|
+
</ScrollView>
|
|
218
|
+
|
|
219
|
+
{/* Bottom navigation */}
|
|
220
|
+
<View variant="transparent" style={styles.bottomBar}>
|
|
221
|
+
{/* Skip */}
|
|
222
|
+
{!isLastSlide ? (
|
|
223
|
+
<TouchableOpacity onPress={onClose} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
|
|
224
|
+
<Text style={styles.skipText}>Skip</Text>
|
|
225
|
+
</TouchableOpacity>
|
|
226
|
+
) : (
|
|
227
|
+
<View variant="transparent" style={{ width: 40 }} />
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
{/* Dots */}
|
|
231
|
+
<View variant="transparent" style={styles.dotsRow}>
|
|
232
|
+
{slides.map((_, i) => (
|
|
233
|
+
<View
|
|
234
|
+
key={i}
|
|
235
|
+
variant="transparent"
|
|
236
|
+
style={[
|
|
237
|
+
styles.dot,
|
|
238
|
+
i === currentSlide
|
|
239
|
+
? { backgroundColor: slides[currentSlide].iconColor, width: 20 }
|
|
240
|
+
: { backgroundColor: 'rgba(255,255,255,0.3)' },
|
|
241
|
+
]}
|
|
242
|
+
/>
|
|
243
|
+
))}
|
|
244
|
+
</View>
|
|
245
|
+
|
|
246
|
+
{/* Next / Get Started */}
|
|
247
|
+
<TouchableOpacity
|
|
248
|
+
onPress={handleNext}
|
|
249
|
+
style={[styles.nextButton, { backgroundColor: slides[currentSlide].iconColor }]}
|
|
250
|
+
activeOpacity={0.8}
|
|
251
|
+
>
|
|
252
|
+
{isLastSlide ? (
|
|
253
|
+
<Text style={styles.nextButtonText}>Let's Go</Text>
|
|
254
|
+
) : (
|
|
255
|
+
<Ionicons name="arrow-forward" size={18} color="#FFF" />
|
|
256
|
+
)}
|
|
257
|
+
</TouchableOpacity>
|
|
258
|
+
</View>
|
|
259
|
+
</Animated.View>
|
|
260
|
+
</Animated.View>
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const styles = StyleSheet.create({
|
|
265
|
+
overlay: {
|
|
266
|
+
...StyleSheet.absoluteFillObject,
|
|
267
|
+
zIndex: 999,
|
|
268
|
+
alignItems: 'center',
|
|
269
|
+
justifyContent: 'center',
|
|
270
|
+
},
|
|
271
|
+
backdrop: {
|
|
272
|
+
...StyleSheet.absoluteFillObject,
|
|
273
|
+
backgroundColor: 'rgba(0,0,0,0.85)',
|
|
274
|
+
},
|
|
275
|
+
card: {
|
|
276
|
+
backgroundColor: '#1a1a2e',
|
|
277
|
+
borderRadius: 24,
|
|
278
|
+
padding: 28,
|
|
279
|
+
alignItems: 'center',
|
|
280
|
+
maxWidth: 420,
|
|
281
|
+
overflow: 'hidden',
|
|
282
|
+
},
|
|
283
|
+
slide: {
|
|
284
|
+
alignItems: 'center',
|
|
285
|
+
paddingTop: 8,
|
|
286
|
+
paddingBottom: 16,
|
|
287
|
+
},
|
|
288
|
+
iconCircle: {
|
|
289
|
+
width: 80,
|
|
290
|
+
height: 80,
|
|
291
|
+
borderRadius: 40,
|
|
292
|
+
alignItems: 'center',
|
|
293
|
+
justifyContent: 'center',
|
|
294
|
+
marginBottom: 20,
|
|
295
|
+
},
|
|
296
|
+
title: {
|
|
297
|
+
color: '#FFFFFF',
|
|
298
|
+
fontSize: 22,
|
|
299
|
+
lineHeight: 28,
|
|
300
|
+
fontWeight: '800',
|
|
301
|
+
textAlign: 'center',
|
|
302
|
+
marginBottom: 12,
|
|
303
|
+
},
|
|
304
|
+
body: {
|
|
305
|
+
color: 'rgba(255,255,255,0.75)',
|
|
306
|
+
fontSize: 15,
|
|
307
|
+
lineHeight: 22,
|
|
308
|
+
textAlign: 'center',
|
|
309
|
+
marginBottom: 16,
|
|
310
|
+
},
|
|
311
|
+
bulletRow: {
|
|
312
|
+
flexDirection: 'row',
|
|
313
|
+
alignItems: 'flex-start',
|
|
314
|
+
width: '100%',
|
|
315
|
+
paddingHorizontal: 4,
|
|
316
|
+
marginBottom: 8,
|
|
317
|
+
},
|
|
318
|
+
bulletDot: {
|
|
319
|
+
width: 6,
|
|
320
|
+
height: 6,
|
|
321
|
+
borderRadius: 3,
|
|
322
|
+
marginTop: 7,
|
|
323
|
+
marginRight: 10,
|
|
324
|
+
},
|
|
325
|
+
bulletText: {
|
|
326
|
+
color: 'rgba(255,255,255,0.65)',
|
|
327
|
+
fontSize: 14,
|
|
328
|
+
lineHeight: 20,
|
|
329
|
+
flex: 1,
|
|
330
|
+
},
|
|
331
|
+
bottomBar: {
|
|
332
|
+
flexDirection: 'row',
|
|
333
|
+
alignItems: 'center',
|
|
334
|
+
justifyContent: 'space-between',
|
|
335
|
+
width: '100%',
|
|
336
|
+
marginTop: 8,
|
|
337
|
+
paddingTop: 16,
|
|
338
|
+
borderTopWidth: 1,
|
|
339
|
+
borderTopColor: 'rgba(255,255,255,0.08)',
|
|
340
|
+
},
|
|
341
|
+
skipText: {
|
|
342
|
+
color: 'rgba(255,255,255,0.4)',
|
|
343
|
+
fontSize: 14,
|
|
344
|
+
lineHeight: 19,
|
|
345
|
+
fontWeight: '600',
|
|
346
|
+
},
|
|
347
|
+
dotsRow: {
|
|
348
|
+
flexDirection: 'row',
|
|
349
|
+
alignItems: 'center',
|
|
350
|
+
gap: 6,
|
|
351
|
+
},
|
|
352
|
+
dot: {
|
|
353
|
+
width: 8,
|
|
354
|
+
height: 8,
|
|
355
|
+
borderRadius: 4,
|
|
356
|
+
},
|
|
357
|
+
nextButton: {
|
|
358
|
+
paddingHorizontal: 16,
|
|
359
|
+
paddingVertical: 8,
|
|
360
|
+
borderRadius: 20,
|
|
361
|
+
alignItems: 'center',
|
|
362
|
+
justifyContent: 'center',
|
|
363
|
+
minWidth: 40,
|
|
364
|
+
},
|
|
365
|
+
nextButtonText: {
|
|
366
|
+
color: '#FFFFFF',
|
|
367
|
+
fontSize: 14,
|
|
368
|
+
lineHeight: 19,
|
|
369
|
+
fontWeight: '700',
|
|
370
|
+
},
|
|
371
|
+
});
|