@dubsdotapp/expo 0.5.17 → 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,683 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ ActivityIndicator,
7
+ Modal,
8
+ Animated,
9
+ StyleSheet,
10
+ ScrollView,
11
+ FlatList,
12
+ TextInput,
13
+ Platform,
14
+ } from 'react-native';
15
+ import { useDubsTheme } from '../theme';
16
+ import { useDubs } from '../../provider';
17
+ import { useJackpot } from '../../hooks/useJackpot';
18
+ import { useEnterJackpot } from '../../hooks/useEnterJackpot';
19
+ import type { EnterJackpotMutationResult } from '../../hooks/useEnterJackpot';
20
+ import type { JackpotEntry } from '../../types';
21
+
22
+ export interface JackpotSheetProps {
23
+ visible: boolean;
24
+ onDismiss: () => void;
25
+ onSuccess?: (result: EnterJackpotMutationResult) => void;
26
+ onError?: (error: Error) => void;
27
+ minEntry?: number;
28
+ maxEntry?: number;
29
+ }
30
+
31
+ const STATUS_LABELS: Record<string, string> = {
32
+ building: 'Building transaction...',
33
+ signing: 'Approve in wallet...',
34
+ confirming: 'Confirming entry...',
35
+ success: "You're in! 🏆",
36
+ };
37
+
38
+ const QUICK_AMOUNTS = [0.1, 0.5, 1];
39
+
40
+ function formatSOL(lamports: string | number): string {
41
+ const val = typeof lamports === 'string' ? parseInt(lamports, 10) : lamports;
42
+ if (isNaN(val) || val === 0) return '0';
43
+ const sol = val / 1_000_000_000;
44
+ if (sol >= 100) return sol.toFixed(0);
45
+ if (sol >= 1) return sol.toFixed(2);
46
+ if (sol >= 0.01) return sol.toFixed(3);
47
+ return sol.toFixed(4);
48
+ }
49
+
50
+ function truncateWallet(addr: string): string {
51
+ if (!addr || addr.length < 8) return addr || '';
52
+ return `${addr.slice(0, 4)}...${addr.slice(-4)}`;
53
+ }
54
+
55
+ export function JackpotSheet({
56
+ visible,
57
+ onDismiss,
58
+ onSuccess,
59
+ onError,
60
+ minEntry = 0.01,
61
+ maxEntry = 10,
62
+ }: JackpotSheetProps) {
63
+ const t = useDubsTheme();
64
+ const { wallet, client } = useDubs();
65
+ const { round, lastWinner, refetch } = useJackpot();
66
+ const mutation = useEnterJackpot();
67
+
68
+ const [betAmount, setBetAmount] = useState('0.1');
69
+ const [entries, setEntries] = useState<JackpotEntry[]>([]);
70
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
71
+
72
+ useEffect(() => {
73
+ Animated.timing(overlayOpacity, {
74
+ toValue: visible ? 1 : 0,
75
+ duration: 250,
76
+ useNativeDriver: true,
77
+ }).start();
78
+ }, [visible, overlayOpacity]);
79
+
80
+ // Fetch entries when sheet opens
81
+ useEffect(() => {
82
+ if (visible) {
83
+ mutation.reset();
84
+ setBetAmount('0.1');
85
+ refetch();
86
+ client.getJackpotEntries().then(res => setEntries(res.entries)).catch(() => {});
87
+ }
88
+ }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
89
+
90
+ // Auto-dismiss on success
91
+ useEffect(() => {
92
+ if (mutation.status === 'success' && mutation.data) {
93
+ onSuccess?.(mutation.data);
94
+ refetch();
95
+ client.getJackpotEntries().then(res => setEntries(res.entries)).catch(() => {});
96
+ const timer = setTimeout(() => onDismiss(), 2500);
97
+ return () => clearTimeout(timer);
98
+ }
99
+ }, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
100
+
101
+ useEffect(() => {
102
+ if (mutation.status === 'error' && mutation.error) {
103
+ onError?.(mutation.error);
104
+ }
105
+ }, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps
106
+
107
+ const potSol = round ? Number(BigInt(round.totalPotLamports || '0')) / 1_000_000_000 : 0;
108
+ const totalWeight = round ? Number(BigInt(round.totalWeight || '0')) : 0;
109
+ const betSol = parseFloat(betAmount) || 0;
110
+ const entryLamports = Math.round(betSol * 1_000_000_000);
111
+
112
+ // Live odds preview
113
+ const userOdds = totalWeight > 0
114
+ ? ((entryLamports / (totalWeight + entryLamports)) * 100).toFixed(1)
115
+ : entries.length === 0 ? '100.0' : '0.0';
116
+
117
+ const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error';
118
+ const canEnter = !isMutating && mutation.status !== 'success' && round?.status === 'Open' && betSol >= minEntry;
119
+
120
+ const handleQuickAdd = useCallback((amount: number) => {
121
+ setBetAmount(prev => {
122
+ const current = parseFloat(prev) || 0;
123
+ const next = Math.min(current + amount, maxEntry);
124
+ return next.toFixed(2);
125
+ });
126
+ }, [maxEntry]);
127
+
128
+ const handleEnter = useCallback(async () => {
129
+ if (!wallet.publicKey || entryLamports < 10000) return;
130
+ try {
131
+ await mutation.execute(entryLamports);
132
+ } catch {
133
+ // Error captured in mutation state
134
+ }
135
+ }, [wallet.publicKey, mutation.execute, entryLamports]); // eslint-disable-line react-hooks/exhaustive-deps
136
+
137
+ const statusLabel = STATUS_LABELS[mutation.status] || '';
138
+
139
+ const renderPlayerCard = ({ item, index }: { item: JackpotEntry; index: number }) => (
140
+ <View style={styles.playerCard}>
141
+ {/* Avatar */}
142
+ <View style={styles.playerAvatar}>
143
+ <Text style={styles.playerAvatarText}>
144
+ {item.player.slice(0, 2).toUpperCase()}
145
+ </Text>
146
+ </View>
147
+ {/* Username */}
148
+ <Text style={styles.playerUsername} numberOfLines={1}>
149
+ @{truncateWallet(item.player)}
150
+ </Text>
151
+ {/* Wager */}
152
+ <View style={styles.playerWagerRow}>
153
+ <Text style={styles.playerWagerAmount}>{item.weightSol.toFixed(2)} SOL</Text>
154
+ </View>
155
+ {/* Odds */}
156
+ <Text style={styles.playerOdds}>{item.oddsPercent}%</Text>
157
+ </View>
158
+ );
159
+
160
+ return (
161
+ <Modal
162
+ visible={visible}
163
+ animationType="slide"
164
+ transparent
165
+ onRequestClose={onDismiss}
166
+ >
167
+ <Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
168
+ <TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
169
+ </Animated.View>
170
+
171
+ <View style={styles.sheetPositioner}>
172
+ <View style={styles.sheet}>
173
+ {/* Drag handle */}
174
+ <View style={styles.handleRow}>
175
+ <View style={styles.handle} />
176
+ </View>
177
+
178
+ <ScrollView
179
+ style={styles.scrollView}
180
+ contentContainerStyle={styles.scrollContent}
181
+ showsVerticalScrollIndicator={false}
182
+ bounces={true}
183
+ >
184
+ {/* Header */}
185
+ <View style={styles.header}>
186
+ <Text style={styles.headerTitle}>Jackpot</Text>
187
+ <TouchableOpacity onPress={onDismiss} activeOpacity={0.8} style={styles.closeBtn}>
188
+ <Text style={styles.closeBtnText}>{'\u2715'}</Text>
189
+ </TouchableOpacity>
190
+ </View>
191
+
192
+ {/* Hero pot card */}
193
+ <View style={styles.heroCard}>
194
+ <View style={styles.heroGradient} />
195
+ <Text style={styles.heroLabel}>JACKPOT</Text>
196
+ <View style={styles.heroPotRow}>
197
+ <Text style={styles.heroPotValue}>{potSol.toFixed(4)} SOL</Text>
198
+ <Text style={styles.heroEmoji}>🤑</Text>
199
+ </View>
200
+ {/* Accent bar */}
201
+ <View style={styles.heroAccentBar} />
202
+ </View>
203
+
204
+ {/* Info cards row */}
205
+ <View style={styles.infoRow}>
206
+ <View style={styles.infoCard}>
207
+ <Text style={styles.infoCardLabel}>YOUR WAGER</Text>
208
+ <Text style={styles.infoCardValue}>{betSol.toFixed(2)}</Text>
209
+ </View>
210
+ <View style={styles.infoCard}>
211
+ <Text style={styles.infoCardLabel}>YOUR CHANCE</Text>
212
+ <Text style={[styles.infoCardValue, { color: '#22c55e' }]}>{userOdds}%</Text>
213
+ </View>
214
+ <View style={styles.infoCard}>
215
+ <Text style={styles.infoCardLabel}>PLAYERS</Text>
216
+ <Text style={styles.infoCardValue}>{entries.length}</Text>
217
+ </View>
218
+ </View>
219
+
220
+ {/* Player carousel */}
221
+ {entries.length > 0 && (
222
+ <View style={styles.playersSection}>
223
+ <View style={styles.playersSectionHeader}>
224
+ <Text style={styles.playersSectionTitle}>Players in Round</Text>
225
+ <View style={styles.playersBadge}>
226
+ <View style={styles.playersBadgeDot} />
227
+ <Text style={styles.playersBadgeText}>{entries.length} active</Text>
228
+ </View>
229
+ </View>
230
+ <FlatList
231
+ data={entries}
232
+ renderItem={renderPlayerCard}
233
+ keyExtractor={(item, i) => `${item.player}-${i}`}
234
+ horizontal
235
+ showsHorizontalScrollIndicator={false}
236
+ contentContainerStyle={styles.playersListContent}
237
+ ItemSeparatorComponent={() => <View style={{ width: 10 }} />}
238
+ />
239
+ </View>
240
+ )}
241
+
242
+ {/* Empty state */}
243
+ {entries.length === 0 && (
244
+ <View style={styles.emptyState}>
245
+ <Text style={styles.emptyEmoji}>🤑</Text>
246
+ <Text style={styles.emptyTitle}>No players yet</Text>
247
+ <Text style={styles.emptySubtitle}>Be the first to join!</Text>
248
+ </View>
249
+ )}
250
+
251
+ {/* Bet input section */}
252
+ <View style={styles.betSection}>
253
+ <Text style={styles.betLabel}>Bet Amount</Text>
254
+
255
+ {/* Input row */}
256
+ <View style={styles.betInputRow}>
257
+ <View style={styles.solIcon}>
258
+ <Text style={styles.solIconText}>◎</Text>
259
+ </View>
260
+ <TextInput
261
+ style={styles.betInput}
262
+ value={betAmount}
263
+ onChangeText={setBetAmount}
264
+ keyboardType="decimal-pad"
265
+ placeholder="0.00"
266
+ placeholderTextColor="#6b6b6b"
267
+ selectTextOnFocus
268
+ />
269
+ <Text style={styles.betInputSuffix}>SOL</Text>
270
+ </View>
271
+
272
+ {/* Quick bet buttons */}
273
+ <View style={styles.quickBetRow}>
274
+ {QUICK_AMOUNTS.map(amount => (
275
+ <TouchableOpacity
276
+ key={amount}
277
+ style={styles.quickBetBtn}
278
+ onPress={() => handleQuickAdd(amount)}
279
+ activeOpacity={0.7}
280
+ >
281
+ <Text style={styles.quickBetText}>+{amount} SOL</Text>
282
+ </TouchableOpacity>
283
+ ))}
284
+ </View>
285
+ </View>
286
+
287
+ {/* Error */}
288
+ {mutation.error && (
289
+ <View style={styles.errorBox}>
290
+ <Text style={styles.errorText}>{mutation.error.message}</Text>
291
+ </View>
292
+ )}
293
+
294
+ {/* Place Bet CTA */}
295
+ <TouchableOpacity
296
+ style={[
297
+ styles.placeBetBtn,
298
+ !canEnter && styles.placeBetBtnDisabled,
299
+ mutation.status === 'success' && styles.placeBetBtnSuccess,
300
+ ]}
301
+ disabled={!canEnter}
302
+ onPress={handleEnter}
303
+ activeOpacity={0.85}
304
+ >
305
+ {isMutating ? (
306
+ <View style={styles.placeBetLoading}>
307
+ <ActivityIndicator size="small" color="#FFFFFF" />
308
+ <Text style={styles.placeBetText}>{statusLabel}</Text>
309
+ </View>
310
+ ) : mutation.status === 'success' ? (
311
+ <Text style={styles.placeBetText}>You're in! 🏆</Text>
312
+ ) : (
313
+ <Text style={[styles.placeBetText, !canEnter && { opacity: 0.5 }]}>
314
+ Place Bet — {betSol.toFixed(2)} SOL
315
+ </Text>
316
+ )}
317
+ </TouchableOpacity>
318
+
319
+ {/* Last winner */}
320
+ {lastWinner && (
321
+ <View style={styles.lastWinnerSection}>
322
+ <Text style={styles.lastWinnerLabel}>Last Winner</Text>
323
+ <View style={styles.lastWinnerRow}>
324
+ <Text style={styles.lastWinnerWallet}>{truncateWallet(lastWinner.winner)}</Text>
325
+ <Text style={styles.lastWinnerAmount}>won {formatSOL(lastWinner.winAmount)} SOL</Text>
326
+ </View>
327
+ </View>
328
+ )}
329
+ </ScrollView>
330
+ </View>
331
+ </View>
332
+ </Modal>
333
+ );
334
+ }
335
+
336
+ const styles = StyleSheet.create({
337
+ overlay: {
338
+ ...StyleSheet.absoluteFillObject,
339
+ backgroundColor: 'rgba(0,0,0,0.65)',
340
+ },
341
+ overlayTap: { flex: 1 },
342
+ sheetPositioner: {
343
+ flex: 1,
344
+ justifyContent: 'flex-end',
345
+ },
346
+ sheet: {
347
+ backgroundColor: '#08080e',
348
+ borderTopLeftRadius: 24,
349
+ borderTopRightRadius: 24,
350
+ maxHeight: '92%',
351
+ borderWidth: 1,
352
+ borderColor: '#1e1e2a',
353
+ borderBottomWidth: 0,
354
+ },
355
+ handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 4 },
356
+ handle: { width: 36, height: 4, borderRadius: 2, backgroundColor: '#2a2a3a' },
357
+ scrollView: { flexGrow: 0 },
358
+ scrollContent: {
359
+ paddingHorizontal: 16,
360
+ paddingBottom: Platform.OS === 'ios' ? 40 : 24,
361
+ },
362
+ // Header
363
+ header: {
364
+ flexDirection: 'row',
365
+ alignItems: 'center',
366
+ justifyContent: 'space-between',
367
+ paddingVertical: 12,
368
+ },
369
+ headerTitle: { fontSize: 20, fontWeight: '700', color: '#FFFFFF' },
370
+ closeBtn: { padding: 4 },
371
+ closeBtnText: { fontSize: 20, color: '#6b6b6b' },
372
+
373
+ // Hero pot card
374
+ heroCard: {
375
+ borderRadius: 16,
376
+ borderWidth: 1,
377
+ borderColor: 'rgba(34, 197, 94, 0.2)',
378
+ padding: 16,
379
+ overflow: 'hidden',
380
+ position: 'relative',
381
+ },
382
+ heroGradient: {
383
+ ...StyleSheet.absoluteFillObject,
384
+ backgroundColor: 'rgba(34, 197, 94, 0.04)',
385
+ },
386
+ heroLabel: {
387
+ fontSize: 10,
388
+ fontWeight: '600',
389
+ color: '#6b6b6b',
390
+ letterSpacing: 3,
391
+ marginBottom: 4,
392
+ },
393
+ heroPotRow: {
394
+ flexDirection: 'row',
395
+ alignItems: 'center',
396
+ justifyContent: 'space-between',
397
+ },
398
+ heroPotValue: {
399
+ fontSize: 32,
400
+ fontWeight: '900',
401
+ color: '#4ade80',
402
+ letterSpacing: -1,
403
+ },
404
+ heroEmoji: {
405
+ fontSize: 36,
406
+ opacity: 0.2,
407
+ },
408
+ heroAccentBar: {
409
+ position: 'absolute',
410
+ bottom: 0,
411
+ left: 0,
412
+ width: '60%',
413
+ height: 2,
414
+ backgroundColor: '#22c55e',
415
+ },
416
+
417
+ // Info row
418
+ infoRow: {
419
+ flexDirection: 'row',
420
+ gap: 8,
421
+ marginTop: 12,
422
+ },
423
+ infoCard: {
424
+ flex: 1,
425
+ borderRadius: 12,
426
+ borderWidth: 1,
427
+ borderColor: '#1e1e2a',
428
+ backgroundColor: '#0c0c14',
429
+ padding: 12,
430
+ },
431
+ infoCardLabel: {
432
+ fontSize: 9,
433
+ fontWeight: '600',
434
+ color: '#6b6b6b',
435
+ letterSpacing: 2,
436
+ marginBottom: 4,
437
+ },
438
+ infoCardValue: {
439
+ fontSize: 18,
440
+ fontWeight: '700',
441
+ color: '#FFFFFF',
442
+ },
443
+
444
+ // Players section
445
+ playersSection: {
446
+ marginTop: 16,
447
+ borderRadius: 16,
448
+ borderWidth: 1,
449
+ borderColor: '#1e1e2a',
450
+ backgroundColor: 'rgba(139, 92, 246, 0.03)',
451
+ padding: 12,
452
+ overflow: 'hidden',
453
+ },
454
+ playersSectionHeader: {
455
+ flexDirection: 'row',
456
+ alignItems: 'center',
457
+ justifyContent: 'space-between',
458
+ marginBottom: 12,
459
+ },
460
+ playersSectionTitle: {
461
+ fontSize: 13,
462
+ fontWeight: '600',
463
+ color: '#a0a0a0',
464
+ },
465
+ playersBadge: {
466
+ flexDirection: 'row',
467
+ alignItems: 'center',
468
+ gap: 6,
469
+ backgroundColor: 'rgba(34, 197, 94, 0.15)',
470
+ paddingHorizontal: 8,
471
+ paddingVertical: 3,
472
+ borderRadius: 100,
473
+ },
474
+ playersBadgeDot: {
475
+ width: 6,
476
+ height: 6,
477
+ borderRadius: 3,
478
+ backgroundColor: '#22c55e',
479
+ },
480
+ playersBadgeText: {
481
+ fontSize: 11,
482
+ fontWeight: '700',
483
+ color: '#22c55e',
484
+ },
485
+ playersListContent: {
486
+ paddingRight: 4,
487
+ },
488
+
489
+ // Player cards (carousel items)
490
+ playerCard: {
491
+ width: 110,
492
+ borderRadius: 12,
493
+ borderWidth: 1.5,
494
+ borderColor: 'rgba(139, 92, 246, 0.4)',
495
+ backgroundColor: 'rgba(139, 92, 246, 0.08)',
496
+ padding: 12,
497
+ alignItems: 'center',
498
+ gap: 6,
499
+ },
500
+ playerAvatar: {
501
+ width: 48,
502
+ height: 48,
503
+ borderRadius: 24,
504
+ borderWidth: 2,
505
+ borderColor: '#8b5cf6',
506
+ backgroundColor: 'rgba(139, 92, 246, 0.15)',
507
+ alignItems: 'center',
508
+ justifyContent: 'center',
509
+ },
510
+ playerAvatarText: {
511
+ fontSize: 16,
512
+ fontWeight: '700',
513
+ color: '#a78bfa',
514
+ },
515
+ playerUsername: {
516
+ fontSize: 11,
517
+ fontWeight: '500',
518
+ color: '#a0a0a0',
519
+ textAlign: 'center',
520
+ },
521
+ playerWagerRow: {
522
+ flexDirection: 'row',
523
+ alignItems: 'center',
524
+ gap: 4,
525
+ },
526
+ playerWagerAmount: {
527
+ fontSize: 13,
528
+ fontWeight: '600',
529
+ color: '#a78bfa',
530
+ },
531
+ playerOdds: {
532
+ fontSize: 11,
533
+ fontWeight: '700',
534
+ color: '#22c55e',
535
+ },
536
+
537
+ // Empty state
538
+ emptyState: {
539
+ marginTop: 16,
540
+ alignItems: 'center',
541
+ paddingVertical: 24,
542
+ borderRadius: 16,
543
+ borderWidth: 1,
544
+ borderColor: '#1e1e2a',
545
+ backgroundColor: '#0c0c14',
546
+ },
547
+ emptyEmoji: { fontSize: 40, marginBottom: 8 },
548
+ emptyTitle: { fontSize: 16, fontWeight: '600', color: '#FFFFFF' },
549
+ emptySubtitle: { fontSize: 13, color: '#6b6b6b', marginTop: 4 },
550
+
551
+ // Bet section
552
+ betSection: {
553
+ marginTop: 16,
554
+ },
555
+ betLabel: {
556
+ fontSize: 13,
557
+ fontWeight: '500',
558
+ color: '#6b6b6b',
559
+ marginBottom: 8,
560
+ },
561
+ betInputRow: {
562
+ flexDirection: 'row',
563
+ alignItems: 'center',
564
+ gap: 8,
565
+ backgroundColor: '#08080e',
566
+ borderWidth: 1,
567
+ borderColor: '#2a2a3a',
568
+ borderRadius: 12,
569
+ paddingHorizontal: 14,
570
+ paddingVertical: Platform.OS === 'ios' ? 14 : 10,
571
+ },
572
+ solIcon: {
573
+ width: 32,
574
+ height: 32,
575
+ borderRadius: 16,
576
+ backgroundColor: 'rgba(139, 92, 246, 0.15)',
577
+ alignItems: 'center',
578
+ justifyContent: 'center',
579
+ },
580
+ solIconText: {
581
+ fontSize: 18,
582
+ color: '#a78bfa',
583
+ fontWeight: '700',
584
+ },
585
+ betInput: {
586
+ flex: 1,
587
+ fontSize: 24,
588
+ fontWeight: '700',
589
+ color: '#FFFFFF',
590
+ padding: 0,
591
+ },
592
+ betInputSuffix: {
593
+ fontSize: 14,
594
+ fontWeight: '500',
595
+ color: '#6b6b6b',
596
+ },
597
+ quickBetRow: {
598
+ flexDirection: 'row',
599
+ gap: 8,
600
+ marginTop: 10,
601
+ },
602
+ quickBetBtn: {
603
+ flex: 1,
604
+ paddingVertical: 12,
605
+ borderRadius: 10,
606
+ borderWidth: 1,
607
+ borderColor: '#2a2a3a',
608
+ backgroundColor: '#08080e',
609
+ alignItems: 'center',
610
+ },
611
+ quickBetText: {
612
+ fontSize: 13,
613
+ fontWeight: '600',
614
+ color: '#FFFFFF',
615
+ },
616
+
617
+ // Error
618
+ errorBox: {
619
+ marginTop: 12,
620
+ borderRadius: 12,
621
+ borderWidth: 1,
622
+ borderColor: '#3A1515',
623
+ backgroundColor: '#1A0A0A',
624
+ padding: 12,
625
+ },
626
+ errorText: { fontSize: 13, fontWeight: '500', color: '#F87171' },
627
+
628
+ // Place Bet CTA
629
+ placeBetBtn: {
630
+ marginTop: 16,
631
+ height: 56,
632
+ borderRadius: 14,
633
+ justifyContent: 'center',
634
+ alignItems: 'center',
635
+ backgroundColor: '#22c55e',
636
+ shadowColor: '#22c55e',
637
+ shadowOffset: { width: 0, height: 4 },
638
+ shadowOpacity: 0.25,
639
+ shadowRadius: 12,
640
+ elevation: 6,
641
+ },
642
+ placeBetBtnDisabled: {
643
+ backgroundColor: '#1e1e2a',
644
+ shadowOpacity: 0,
645
+ elevation: 0,
646
+ },
647
+ placeBetBtnSuccess: {
648
+ backgroundColor: '#16a34a',
649
+ },
650
+ placeBetText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' },
651
+ placeBetLoading: { flexDirection: 'row', alignItems: 'center', gap: 10 },
652
+
653
+ // Last winner
654
+ lastWinnerSection: {
655
+ marginTop: 14,
656
+ paddingTop: 14,
657
+ borderTopWidth: 1,
658
+ borderTopColor: '#1e1e2a',
659
+ },
660
+ lastWinnerLabel: {
661
+ fontSize: 11,
662
+ fontWeight: '600',
663
+ color: '#6b6b6b',
664
+ letterSpacing: 1,
665
+ textTransform: 'uppercase',
666
+ marginBottom: 4,
667
+ },
668
+ lastWinnerRow: {
669
+ flexDirection: 'row',
670
+ alignItems: 'center',
671
+ gap: 6,
672
+ },
673
+ lastWinnerWallet: {
674
+ fontSize: 13,
675
+ fontWeight: '500',
676
+ color: '#a0a0a0',
677
+ },
678
+ lastWinnerAmount: {
679
+ fontSize: 13,
680
+ fontWeight: '600',
681
+ color: '#22c55e',
682
+ },
683
+ });