@dubsdotapp/expo 0.5.16 → 0.5.18

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.
@@ -0,0 +1,417 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ Animated,
7
+ StyleSheet,
8
+ Image,
9
+ } from 'react-native';
10
+ import type { JackpotRound, JackpotLastWinner, JackpotEntry } from '../../types';
11
+ import type { ViewStyle } from 'react-native';
12
+
13
+ export interface JackpotCardProps {
14
+ round: JackpotRound | null;
15
+ lastWinner?: JackpotLastWinner | null;
16
+ entries?: JackpotEntry[];
17
+ onPress?: () => void;
18
+ style?: ViewStyle;
19
+ }
20
+
21
+ function formatSOL(lamports: string | number): string {
22
+ const val = typeof lamports === 'string' ? parseInt(lamports, 10) : lamports;
23
+ if (isNaN(val) || val === 0) return '0';
24
+ const sol = val / 1_000_000_000;
25
+ // No trailing zeros
26
+ if (sol >= 100) return sol.toFixed(0);
27
+ if (sol >= 1) return sol.toFixed(2);
28
+ if (sol >= 0.01) return sol.toFixed(3);
29
+ return sol.toFixed(4);
30
+ }
31
+
32
+ function truncateWallet(addr: string): string {
33
+ if (!addr || addr.length < 8) return addr || '';
34
+ return `${addr.slice(0, 4)}...${addr.slice(-4)}`;
35
+ }
36
+
37
+ export function JackpotCard({ round, lastWinner, entries, onPress, style }: JackpotCardProps) {
38
+ const shimmerAnim = useRef(new Animated.Value(-1)).current;
39
+ const pulseAnim = useRef(new Animated.Value(0.4)).current;
40
+
41
+ useEffect(() => {
42
+ // Shimmer sweep: matches web heroSweep (4s)
43
+ const shimmer = Animated.loop(
44
+ Animated.sequence([
45
+ Animated.timing(shimmerAnim, { toValue: 1, duration: 4000, useNativeDriver: true }),
46
+ Animated.delay(500),
47
+ Animated.timing(shimmerAnim, { toValue: -1, duration: 0, useNativeDriver: true }),
48
+ ]),
49
+ );
50
+ shimmer.start();
51
+
52
+ // Status dot pulse
53
+ const pulse = Animated.loop(
54
+ Animated.sequence([
55
+ Animated.timing(pulseAnim, { toValue: 1, duration: 1000, useNativeDriver: true }),
56
+ Animated.timing(pulseAnim, { toValue: 0.4, duration: 1000, useNativeDriver: true }),
57
+ ]),
58
+ );
59
+ pulse.start();
60
+
61
+ return () => { shimmer.stop(); pulse.stop(); };
62
+ }, [shimmerAnim, pulseAnim]);
63
+
64
+ const potSol = round ? formatSOL(round.totalPotLamports) : '0';
65
+ const isOpen = round?.status === 'Open';
66
+ const entryCount = round?.entryCount ?? 0;
67
+ const totalWeight = round ? Number(BigInt(round.totalWeight || '0')) : 0;
68
+
69
+ return (
70
+ <TouchableOpacity
71
+ activeOpacity={0.9}
72
+ onPress={onPress}
73
+ style={[styles.card, style]}
74
+ >
75
+ {/* Green accent gradient background */}
76
+ <View style={styles.gradientBg} />
77
+
78
+ {/* Shimmer sweep overlay */}
79
+ <Animated.View
80
+ style={[
81
+ styles.shimmer,
82
+ {
83
+ transform: [{
84
+ translateX: shimmerAnim.interpolate({
85
+ inputRange: [-1, 1],
86
+ outputRange: [-400, 400],
87
+ }),
88
+ }],
89
+ },
90
+ ]}
91
+ />
92
+
93
+ {/* Bottom accent bar */}
94
+ <View style={styles.accentBar} />
95
+
96
+ {/* Content */}
97
+ <View style={styles.content}>
98
+ {/* Status badge */}
99
+ <View style={styles.statusRow}>
100
+ <View style={[styles.statusBadge, isOpen ? styles.statusOpen : styles.statusClosed]}>
101
+ {isOpen && (
102
+ <Animated.View style={[styles.statusDot, { opacity: pulseAnim }]} />
103
+ )}
104
+ <Text style={[styles.statusText, { color: isOpen ? '#22c55e' : '#9ca3af' }]}>
105
+ {isOpen ? 'Open' : round?.status ?? 'Loading'}
106
+ </Text>
107
+ </View>
108
+ <Text style={styles.entryCountText}>{entryCount} player{entryCount !== 1 ? 's' : ''}</Text>
109
+ </View>
110
+
111
+ {/* Hero pot section */}
112
+ <View style={styles.heroSection}>
113
+ <View style={styles.potInfo}>
114
+ <Text style={styles.jackpotLabel}>JACKPOT</Text>
115
+ <Text style={styles.potValue}>{potSol} SOL</Text>
116
+ </View>
117
+ <Text style={styles.potEmoji}>🤑</Text>
118
+ </View>
119
+
120
+ {/* Info grid: Your Chance / Players / Last Winner */}
121
+ <View style={styles.infoGrid}>
122
+ <View style={styles.infoCard}>
123
+ <Text style={styles.infoLabel}>PLAYERS</Text>
124
+ <Text style={styles.infoValue}>{entryCount}</Text>
125
+ </View>
126
+ <View style={styles.infoCard}>
127
+ <Text style={styles.infoLabel}>TOTAL POT</Text>
128
+ <Text style={[styles.infoValue, { color: '#4ade80' }]}>{potSol}</Text>
129
+ </View>
130
+ <View style={styles.infoCard}>
131
+ <Text style={styles.infoLabel}>LAST WIN</Text>
132
+ <Text style={[styles.infoValue, { color: '#22c55e' }]}>
133
+ {lastWinner ? formatSOL(lastWinner.winAmount) : '—'}
134
+ </Text>
135
+ </View>
136
+ </View>
137
+
138
+ {/* Player carousel (horizontal scroll of entry chips) */}
139
+ {entries && entries.length > 0 && (
140
+ <View style={styles.playersSection}>
141
+ <View style={styles.playersSectionHeader}>
142
+ <Text style={styles.playersSectionTitle}>Players in Round</Text>
143
+ <View style={styles.activeCountBadge}>
144
+ <Text style={styles.activeCountText}>{entries.length}</Text>
145
+ </View>
146
+ </View>
147
+ <View style={styles.playersCarousel}>
148
+ {entries.slice(0, 8).map((entry, i) => {
149
+ const odds = entry.oddsPercent;
150
+ return (
151
+ <View key={`${entry.player}-${i}`} style={styles.playerCard}>
152
+ {/* Avatar placeholder */}
153
+ <View style={styles.playerAvatar}>
154
+ <Text style={styles.playerAvatarText}>
155
+ {entry.player.slice(0, 2).toUpperCase()}
156
+ </Text>
157
+ </View>
158
+ <Text style={styles.playerWallet} numberOfLines={1}>
159
+ {truncateWallet(entry.player)}
160
+ </Text>
161
+ <Text style={styles.playerWager}>{entry.weightSol.toFixed(2)} SOL</Text>
162
+ <Text style={styles.playerOdds}>{odds}%</Text>
163
+ </View>
164
+ );
165
+ })}
166
+ {entries.length > 8 && (
167
+ <View style={styles.playerCardMore}>
168
+ <Text style={styles.playerMoreText}>+{entries.length - 8}</Text>
169
+ </View>
170
+ )}
171
+ </View>
172
+ </View>
173
+ )}
174
+
175
+ {/* Place Bet CTA */}
176
+ <TouchableOpacity
177
+ style={styles.placeBetButton}
178
+ activeOpacity={0.85}
179
+ onPress={onPress}
180
+ >
181
+ <Text style={styles.placeBetText}>Place Bet</Text>
182
+ </TouchableOpacity>
183
+ </View>
184
+ </TouchableOpacity>
185
+ );
186
+ }
187
+
188
+ const styles = StyleSheet.create({
189
+ card: {
190
+ borderRadius: 16,
191
+ borderWidth: 1,
192
+ borderColor: 'rgba(34, 197, 94, 0.2)',
193
+ backgroundColor: '#0c0c14',
194
+ overflow: 'hidden',
195
+ position: 'relative',
196
+ },
197
+ gradientBg: {
198
+ ...StyleSheet.absoluteFillObject,
199
+ backgroundColor: 'rgba(34, 197, 94, 0.04)',
200
+ },
201
+ shimmer: {
202
+ position: 'absolute',
203
+ top: 0,
204
+ bottom: 0,
205
+ width: 120,
206
+ backgroundColor: 'rgba(34, 197, 94, 0.08)',
207
+ },
208
+ accentBar: {
209
+ position: 'absolute',
210
+ bottom: 0,
211
+ left: 0,
212
+ right: '40%' as any,
213
+ height: 2,
214
+ backgroundColor: '#22c55e',
215
+ },
216
+ content: {
217
+ padding: 16,
218
+ gap: 12,
219
+ },
220
+ // Status row
221
+ statusRow: {
222
+ flexDirection: 'row',
223
+ alignItems: 'center',
224
+ gap: 8,
225
+ },
226
+ statusBadge: {
227
+ flexDirection: 'row',
228
+ alignItems: 'center',
229
+ gap: 6,
230
+ paddingHorizontal: 10,
231
+ paddingVertical: 4,
232
+ borderRadius: 100,
233
+ },
234
+ statusOpen: {
235
+ backgroundColor: 'rgba(34, 197, 94, 0.15)',
236
+ },
237
+ statusClosed: {
238
+ backgroundColor: 'rgba(156, 163, 175, 0.15)',
239
+ },
240
+ statusDot: {
241
+ width: 6,
242
+ height: 6,
243
+ borderRadius: 3,
244
+ backgroundColor: '#22c55e',
245
+ },
246
+ statusText: {
247
+ fontSize: 12,
248
+ fontWeight: '700',
249
+ },
250
+ entryCountText: {
251
+ fontSize: 12,
252
+ color: '#6b6b6b',
253
+ fontWeight: '500',
254
+ },
255
+ // Hero pot
256
+ heroSection: {
257
+ flexDirection: 'row',
258
+ alignItems: 'center',
259
+ justifyContent: 'space-between',
260
+ },
261
+ potInfo: {
262
+ flex: 1,
263
+ },
264
+ jackpotLabel: {
265
+ fontSize: 10,
266
+ fontWeight: '600',
267
+ color: '#6b6b6b',
268
+ letterSpacing: 3,
269
+ textTransform: 'uppercase',
270
+ marginBottom: 4,
271
+ },
272
+ potValue: {
273
+ fontSize: 36,
274
+ fontWeight: '900',
275
+ color: '#4ade80',
276
+ letterSpacing: -1,
277
+ },
278
+ potEmoji: {
279
+ fontSize: 40,
280
+ opacity: 0.2,
281
+ },
282
+ // Info grid
283
+ infoGrid: {
284
+ flexDirection: 'row',
285
+ gap: 8,
286
+ },
287
+ infoCard: {
288
+ flex: 1,
289
+ borderRadius: 12,
290
+ borderWidth: 1,
291
+ borderColor: '#1e1e2a',
292
+ backgroundColor: '#0c0c14',
293
+ padding: 12,
294
+ },
295
+ infoLabel: {
296
+ fontSize: 9,
297
+ fontWeight: '600',
298
+ color: '#6b6b6b',
299
+ letterSpacing: 2,
300
+ textTransform: 'uppercase',
301
+ marginBottom: 4,
302
+ },
303
+ infoValue: {
304
+ fontSize: 18,
305
+ fontWeight: '700',
306
+ color: '#FFFFFF',
307
+ },
308
+ // Players section
309
+ playersSection: {
310
+ borderRadius: 12,
311
+ borderWidth: 1,
312
+ borderColor: '#1e1e2a',
313
+ backgroundColor: 'rgba(139, 92, 246, 0.04)',
314
+ padding: 12,
315
+ overflow: 'hidden',
316
+ },
317
+ playersSectionHeader: {
318
+ flexDirection: 'row',
319
+ alignItems: 'center',
320
+ justifyContent: 'space-between',
321
+ marginBottom: 10,
322
+ },
323
+ playersSectionTitle: {
324
+ fontSize: 13,
325
+ fontWeight: '600',
326
+ color: '#a0a0a0',
327
+ },
328
+ activeCountBadge: {
329
+ backgroundColor: 'rgba(34, 197, 94, 0.15)',
330
+ paddingHorizontal: 8,
331
+ paddingVertical: 2,
332
+ borderRadius: 100,
333
+ },
334
+ activeCountText: {
335
+ fontSize: 11,
336
+ fontWeight: '700',
337
+ color: '#22c55e',
338
+ },
339
+ playersCarousel: {
340
+ flexDirection: 'row',
341
+ gap: 10,
342
+ },
343
+ playerCard: {
344
+ width: 96,
345
+ borderRadius: 12,
346
+ borderWidth: 1.5,
347
+ borderColor: 'rgba(139, 92, 246, 0.4)',
348
+ backgroundColor: 'rgba(139, 92, 246, 0.08)',
349
+ padding: 10,
350
+ alignItems: 'center',
351
+ gap: 4,
352
+ },
353
+ playerAvatar: {
354
+ width: 40,
355
+ height: 40,
356
+ borderRadius: 20,
357
+ borderWidth: 1.5,
358
+ borderColor: '#8b5cf6',
359
+ backgroundColor: 'rgba(139, 92, 246, 0.2)',
360
+ alignItems: 'center',
361
+ justifyContent: 'center',
362
+ },
363
+ playerAvatarText: {
364
+ fontSize: 14,
365
+ fontWeight: '700',
366
+ color: '#a78bfa',
367
+ },
368
+ playerWallet: {
369
+ fontSize: 10,
370
+ fontWeight: '500',
371
+ color: '#a0a0a0',
372
+ width: '100%',
373
+ textAlign: 'center',
374
+ },
375
+ playerWager: {
376
+ fontSize: 12,
377
+ fontWeight: '600',
378
+ color: '#a78bfa',
379
+ },
380
+ playerOdds: {
381
+ fontSize: 10,
382
+ fontWeight: '700',
383
+ color: '#22c55e',
384
+ },
385
+ playerCardMore: {
386
+ width: 96,
387
+ borderRadius: 12,
388
+ borderWidth: 1.5,
389
+ borderColor: '#1e1e2a',
390
+ backgroundColor: '#14141e',
391
+ alignItems: 'center',
392
+ justifyContent: 'center',
393
+ },
394
+ playerMoreText: {
395
+ fontSize: 16,
396
+ fontWeight: '700',
397
+ color: '#6b6b6b',
398
+ },
399
+ // Place bet CTA
400
+ placeBetButton: {
401
+ height: 52,
402
+ borderRadius: 12,
403
+ alignItems: 'center',
404
+ justifyContent: 'center',
405
+ backgroundColor: '#22c55e',
406
+ shadowColor: '#22c55e',
407
+ shadowOffset: { width: 0, height: 4 },
408
+ shadowOpacity: 0.25,
409
+ shadowRadius: 12,
410
+ elevation: 6,
411
+ },
412
+ placeBetText: {
413
+ color: '#FFFFFF',
414
+ fontSize: 16,
415
+ fontWeight: '700',
416
+ },
417
+ });