@dubsdotapp/expo 0.5.6 → 0.5.8
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/dist/index.d.mts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +613 -266
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +591 -238
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/ui/game/JoinGameSheet.tsx +187 -22
- package/src/ui/game/SolSlider.tsx +259 -0
- package/src/ui/game/index.ts +2 -0
- package/src/ui/index.ts +2 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -134,7 +134,8 @@ export { AuthGate, ConnectWalletScreen, ConnectWalletButton, UserProfileCard, Se
|
|
|
134
134
|
export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, ConnectWalletButtonProps, AuthGateConnectWalletProps, UserProfileCardProps, SettingsSheetProps, UserProfileSheetProps, DubsTheme } from './ui';
|
|
135
135
|
|
|
136
136
|
// Game widgets
|
|
137
|
-
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet } from './ui';
|
|
137
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './ui';
|
|
138
|
+
export type { SolSliderProps } from './ui';
|
|
138
139
|
export type {
|
|
139
140
|
GamePosterProps,
|
|
140
141
|
LivePoolsCardProps,
|
|
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
+
Image,
|
|
5
6
|
TouchableOpacity,
|
|
6
7
|
ActivityIndicator,
|
|
7
8
|
Modal,
|
|
@@ -15,6 +16,7 @@ import { useDubs } from '../../provider';
|
|
|
15
16
|
import { useJoinGame } from '../../hooks/useJoinGame';
|
|
16
17
|
import type { JoinGameMutationResult } from '../../hooks/useJoinGame';
|
|
17
18
|
import type { GameDetail } from '../../types';
|
|
19
|
+
import { SolSlider } from './SolSlider';
|
|
18
20
|
|
|
19
21
|
export interface JoinGameSheetProps {
|
|
20
22
|
visible: boolean;
|
|
@@ -32,6 +34,12 @@ export interface JoinGameSheetProps {
|
|
|
32
34
|
onError?: (error: Error) => void;
|
|
33
35
|
/** Called when the user taps a team card (useful for sound/haptics) */
|
|
34
36
|
onTeamSelect?: (team: 'home' | 'away') => void;
|
|
37
|
+
/** Called when the join succeeds (useful for success sound/animation) */
|
|
38
|
+
onJoinSuccess?: (result: JoinGameMutationResult) => void;
|
|
39
|
+
/** Called on each slider tick (useful for haptics/sound) */
|
|
40
|
+
onSliderTick?: (value: number) => void;
|
|
41
|
+
/** Max wager in SOL (default 5) */
|
|
42
|
+
maxWager?: number;
|
|
35
43
|
/** Pool mode: hides team selection, auto-assigns team, shows "Join Pool" labels */
|
|
36
44
|
isPoolModeEnabled?: boolean;
|
|
37
45
|
}
|
|
@@ -45,6 +53,16 @@ const STATUS_LABELS: Record<string, string> = {
|
|
|
45
53
|
|
|
46
54
|
const CUSTOM_GAME_MODE = 6;
|
|
47
55
|
|
|
56
|
+
function formatSol(n: number): string {
|
|
57
|
+
// Remove trailing zeros: 0.06000000 → 0.06, 1.00000 → 1
|
|
58
|
+
return parseFloat(n.toFixed(9)).toString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toPng(url: string | null | undefined): string | null {
|
|
62
|
+
if (!url) return null;
|
|
63
|
+
return url.replace('/svg?', '/png?');
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
export function JoinGameSheet({
|
|
49
67
|
visible,
|
|
50
68
|
onDismiss,
|
|
@@ -56,6 +74,9 @@ export function JoinGameSheet({
|
|
|
56
74
|
onSuccess,
|
|
57
75
|
onError,
|
|
58
76
|
onTeamSelect,
|
|
77
|
+
onJoinSuccess,
|
|
78
|
+
onSliderTick,
|
|
79
|
+
maxWager = 5,
|
|
59
80
|
isPoolModeEnabled = false,
|
|
60
81
|
}: JoinGameSheetProps) {
|
|
61
82
|
const t = useDubsTheme();
|
|
@@ -64,11 +85,13 @@ export function JoinGameSheet({
|
|
|
64
85
|
|
|
65
86
|
const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
|
|
66
87
|
|
|
67
|
-
// Pool mode and custom games auto-assign team — no team selection needed.
|
|
68
|
-
// For sports/esports games the user picks a team.
|
|
69
88
|
const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
|
|
89
|
+
const [wager, setWager] = useState(game.buyIn);
|
|
90
|
+
const [showSuccess, setShowSuccess] = useState(false);
|
|
70
91
|
|
|
71
92
|
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
|
93
|
+
const successScale = useRef(new Animated.Value(0)).current;
|
|
94
|
+
const successOpacity = useRef(new Animated.Value(0)).current;
|
|
72
95
|
|
|
73
96
|
// Animate overlay on visibility change
|
|
74
97
|
useEffect(() => {
|
|
@@ -83,17 +106,32 @@ export function JoinGameSheet({
|
|
|
83
106
|
useEffect(() => {
|
|
84
107
|
if (visible) {
|
|
85
108
|
setSelectedTeam(isPoolModeEnabled ? 'home' : isCustomGame ? 'away' : null);
|
|
109
|
+
setWager(game.buyIn);
|
|
110
|
+
setShowSuccess(false);
|
|
111
|
+
successScale.setValue(0);
|
|
112
|
+
successOpacity.setValue(0);
|
|
86
113
|
mutation.reset();
|
|
87
114
|
}
|
|
88
115
|
}, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
89
116
|
|
|
90
|
-
//
|
|
117
|
+
// Handle success with animation
|
|
91
118
|
useEffect(() => {
|
|
92
119
|
if (mutation.status === 'success' && mutation.data) {
|
|
120
|
+
setShowSuccess(true);
|
|
93
121
|
onSuccess?.(mutation.data);
|
|
122
|
+
onJoinSuccess?.(mutation.data);
|
|
123
|
+
|
|
124
|
+
// Animate success
|
|
125
|
+
Animated.parallel([
|
|
126
|
+
Animated.spring(successScale, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }),
|
|
127
|
+
Animated.timing(successOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
128
|
+
]).start();
|
|
129
|
+
|
|
94
130
|
const timer = setTimeout(() => {
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
Animated.timing(successOpacity, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
|
|
132
|
+
onDismiss();
|
|
133
|
+
});
|
|
134
|
+
}, 2500);
|
|
97
135
|
return () => clearTimeout(timer);
|
|
98
136
|
}
|
|
99
137
|
}, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -114,16 +152,20 @@ export function JoinGameSheet({
|
|
|
114
152
|
const awayPool = game.awayPool || 0;
|
|
115
153
|
const buyIn = game.buyIn;
|
|
116
154
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
155
|
+
const poolAfterJoin = totalPool + wager;
|
|
156
|
+
|
|
157
|
+
const { homeOdds, awayOdds, homeBets, awayBets } = useMemo(() => {
|
|
158
|
+
const newPool = totalPool + wager;
|
|
159
|
+
return {
|
|
160
|
+
homeOdds: homePool > 0 ? (newPool / (homePool + (selectedTeam === 'home' ? wager : 0))).toFixed(2) : '—',
|
|
161
|
+
awayOdds: awayPool > 0 ? (newPool / (awayPool + (selectedTeam === 'away' ? wager : 0))).toFixed(2) : '—',
|
|
162
|
+
homeBets: bettors.filter(b => b.team === 'home').length,
|
|
163
|
+
awayBets: bettors.filter(b => b.team === 'away').length,
|
|
164
|
+
};
|
|
165
|
+
}, [totalPool, homePool, awayPool, bettors, wager, selectedTeam]);
|
|
123
166
|
|
|
124
|
-
const poolAfterJoin = totalPool + buyIn;
|
|
125
167
|
const selectedOdds = selectedTeam === 'home' ? homeOdds : selectedTeam === 'away' ? awayOdds : '—';
|
|
126
|
-
const potentialWinnings = selectedOdds !== '—' ? (parseFloat(selectedOdds) *
|
|
168
|
+
const potentialWinnings = selectedOdds !== '—' ? formatSol(parseFloat(selectedOdds) * wager) : '—';
|
|
127
169
|
|
|
128
170
|
const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
|
|
129
171
|
const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
|
|
@@ -146,12 +188,12 @@ export function JoinGameSheet({
|
|
|
146
188
|
playerWallet: wallet.publicKey.toBase58(),
|
|
147
189
|
gameId: game.gameId,
|
|
148
190
|
teamChoice: selectedTeam,
|
|
149
|
-
amount:
|
|
191
|
+
amount: wager,
|
|
150
192
|
});
|
|
151
193
|
} catch {
|
|
152
194
|
// Error is already captured in mutation state
|
|
153
195
|
}
|
|
154
|
-
}, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId,
|
|
196
|
+
}, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId, wager]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
155
197
|
|
|
156
198
|
const statusLabel = STATUS_LABELS[mutation.status] || '';
|
|
157
199
|
|
|
@@ -166,6 +208,19 @@ export function JoinGameSheet({
|
|
|
166
208
|
<TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
|
|
167
209
|
</Animated.View>
|
|
168
210
|
|
|
211
|
+
{/* Success overlay */}
|
|
212
|
+
{showSuccess && (
|
|
213
|
+
<View style={styles.successOverlay}>
|
|
214
|
+
<Animated.View style={[styles.successContent, { opacity: successOpacity, transform: [{ scale: successScale }] }]}>
|
|
215
|
+
<Text style={styles.successEmoji}>🎉</Text>
|
|
216
|
+
<Text style={styles.successTitle}>You're in!</Text>
|
|
217
|
+
<Text style={styles.successSub}>
|
|
218
|
+
{formatSol(buyIn)} SOL on {selectedName}
|
|
219
|
+
</Text>
|
|
220
|
+
</Animated.View>
|
|
221
|
+
</View>
|
|
222
|
+
)}
|
|
223
|
+
|
|
169
224
|
<KeyboardAvoidingView
|
|
170
225
|
style={styles.keyboardView}
|
|
171
226
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
@@ -185,6 +240,34 @@ export function JoinGameSheet({
|
|
|
185
240
|
</TouchableOpacity>
|
|
186
241
|
</View>
|
|
187
242
|
|
|
243
|
+
{/* Bettors row */}
|
|
244
|
+
{bettors.length > 0 && (
|
|
245
|
+
<View style={styles.bettorsSection}>
|
|
246
|
+
<Text style={[styles.bettorsLabel, { color: t.textMuted }]}>
|
|
247
|
+
{bettors.length} {bettors.length === 1 ? 'player' : 'players'} in this game
|
|
248
|
+
</Text>
|
|
249
|
+
<View style={styles.bettorsRow}>
|
|
250
|
+
{bettors.slice(0, 6).map((b, i) => {
|
|
251
|
+
const pngUrl = toPng(b.avatar);
|
|
252
|
+
return (
|
|
253
|
+
<View key={b.wallet} style={[styles.bettorCircle, { backgroundColor: ['#EF4444','#3B82F6','#22C55E','#F59E0B','#A855F7','#EC4899'][i % 6] }]}>
|
|
254
|
+
{pngUrl ? (
|
|
255
|
+
<Image source={{ uri: pngUrl }} style={styles.bettorImg} />
|
|
256
|
+
) : (
|
|
257
|
+
<Text style={styles.bettorInitial}>{(b.username ?? b.wallet).charAt(0).toUpperCase()}</Text>
|
|
258
|
+
)}
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
})}
|
|
262
|
+
{bettors.length > 6 && (
|
|
263
|
+
<View style={[styles.bettorCircle, { backgroundColor: '#2C2C2E' }]}>
|
|
264
|
+
<Text style={styles.bettorOverflow}>+{bettors.length - 6}</Text>
|
|
265
|
+
</View>
|
|
266
|
+
)}
|
|
267
|
+
</View>
|
|
268
|
+
</View>
|
|
269
|
+
)}
|
|
270
|
+
|
|
188
271
|
{/* Team Selection — hidden in pool mode and custom games */}
|
|
189
272
|
{!isCustomGame && !isPoolModeEnabled && (
|
|
190
273
|
<View style={styles.section}>
|
|
@@ -216,11 +299,26 @@ export function JoinGameSheet({
|
|
|
216
299
|
</View>
|
|
217
300
|
)}
|
|
218
301
|
|
|
302
|
+
{/* SOL Slider */}
|
|
303
|
+
{selectedTeam && !isPoolModeEnabled && (
|
|
304
|
+
<View style={styles.sliderSection}>
|
|
305
|
+
<SolSlider
|
|
306
|
+
value={wager}
|
|
307
|
+
min={game.buyIn}
|
|
308
|
+
max={maxWager}
|
|
309
|
+
step={0.01}
|
|
310
|
+
accentColor={selectedTeam === 'home' ? homeColor : awayColor}
|
|
311
|
+
onValueChange={setWager}
|
|
312
|
+
onTick={onSliderTick}
|
|
313
|
+
/>
|
|
314
|
+
</View>
|
|
315
|
+
)}
|
|
316
|
+
|
|
219
317
|
{/* Summary Card */}
|
|
220
318
|
<View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
|
|
221
319
|
<View style={styles.summaryRow}>
|
|
222
|
-
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>
|
|
223
|
-
<Text style={[styles.summaryValue, { color: t.text }]}>{
|
|
320
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your wager</Text>
|
|
321
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(wager)} SOL</Text>
|
|
224
322
|
</View>
|
|
225
323
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
226
324
|
{isPoolModeEnabled ? (
|
|
@@ -232,7 +330,7 @@ export function JoinGameSheet({
|
|
|
232
330
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
233
331
|
<View style={styles.summaryRow}>
|
|
234
332
|
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Current pot</Text>
|
|
235
|
-
<Text style={[styles.summaryValue, { color: t.success }]}>{totalPool} SOL</Text>
|
|
333
|
+
<Text style={[styles.summaryValue, { color: t.success }]}>{formatSol(totalPool)} SOL</Text>
|
|
236
334
|
</View>
|
|
237
335
|
</>
|
|
238
336
|
) : (
|
|
@@ -244,7 +342,7 @@ export function JoinGameSheet({
|
|
|
244
342
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
245
343
|
<View style={styles.summaryRow}>
|
|
246
344
|
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
|
|
247
|
-
<Text style={[styles.summaryValue, { color: t.text }]}>{poolAfterJoin} SOL</Text>
|
|
345
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(poolAfterJoin)} SOL</Text>
|
|
248
346
|
</View>
|
|
249
347
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
250
348
|
<View style={styles.summaryRow}>
|
|
@@ -293,9 +391,9 @@ export function JoinGameSheet({
|
|
|
293
391
|
{alreadyJoined
|
|
294
392
|
? 'Already Joined'
|
|
295
393
|
: isPoolModeEnabled
|
|
296
|
-
? `Join Pool \u2014 ${
|
|
394
|
+
? `Join Pool \u2014 ${formatSol(wager)} SOL`
|
|
297
395
|
: selectedTeam
|
|
298
|
-
? `Join Game \u2014 ${
|
|
396
|
+
? `Join Game \u2014 ${formatSol(wager)} SOL`
|
|
299
397
|
: 'Pick a side to join'}
|
|
300
398
|
</Text>
|
|
301
399
|
)}
|
|
@@ -389,6 +487,70 @@ const styles = StyleSheet.create({
|
|
|
389
487
|
fontSize: 20,
|
|
390
488
|
padding: 4,
|
|
391
489
|
},
|
|
490
|
+
|
|
491
|
+
// Bettors row
|
|
492
|
+
bettorsSection: {
|
|
493
|
+
paddingBottom: 12,
|
|
494
|
+
gap: 8,
|
|
495
|
+
},
|
|
496
|
+
bettorsLabel: {
|
|
497
|
+
fontSize: 12,
|
|
498
|
+
fontWeight: '600',
|
|
499
|
+
},
|
|
500
|
+
bettorsRow: {
|
|
501
|
+
flexDirection: 'row',
|
|
502
|
+
alignItems: 'center',
|
|
503
|
+
gap: 4,
|
|
504
|
+
},
|
|
505
|
+
bettorCircle: {
|
|
506
|
+
width: 28,
|
|
507
|
+
height: 28,
|
|
508
|
+
borderRadius: 14,
|
|
509
|
+
alignItems: 'center',
|
|
510
|
+
justifyContent: 'center',
|
|
511
|
+
overflow: 'hidden',
|
|
512
|
+
},
|
|
513
|
+
bettorImg: {
|
|
514
|
+
width: 28,
|
|
515
|
+
height: 28,
|
|
516
|
+
borderRadius: 14,
|
|
517
|
+
},
|
|
518
|
+
bettorInitial: {
|
|
519
|
+
color: '#FFF',
|
|
520
|
+
fontSize: 11,
|
|
521
|
+
fontWeight: '800',
|
|
522
|
+
},
|
|
523
|
+
bettorOverflow: {
|
|
524
|
+
color: '#8E8E93',
|
|
525
|
+
fontSize: 10,
|
|
526
|
+
fontWeight: '700',
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
// Success overlay
|
|
530
|
+
successOverlay: {
|
|
531
|
+
...StyleSheet.absoluteFillObject,
|
|
532
|
+
zIndex: 100,
|
|
533
|
+
alignItems: 'center',
|
|
534
|
+
justifyContent: 'center',
|
|
535
|
+
backgroundColor: 'rgba(0,0,0,0.85)',
|
|
536
|
+
},
|
|
537
|
+
successContent: {
|
|
538
|
+
alignItems: 'center',
|
|
539
|
+
gap: 12,
|
|
540
|
+
},
|
|
541
|
+
successEmoji: {
|
|
542
|
+
fontSize: 64,
|
|
543
|
+
},
|
|
544
|
+
successTitle: {
|
|
545
|
+
color: '#FFFFFF',
|
|
546
|
+
fontSize: 28,
|
|
547
|
+
fontWeight: '900',
|
|
548
|
+
},
|
|
549
|
+
successSub: {
|
|
550
|
+
color: '#8E8E93',
|
|
551
|
+
fontSize: 16,
|
|
552
|
+
},
|
|
553
|
+
|
|
392
554
|
section: {
|
|
393
555
|
gap: 12,
|
|
394
556
|
paddingTop: 8,
|
|
@@ -401,8 +563,11 @@ const styles = StyleSheet.create({
|
|
|
401
563
|
flexDirection: 'row',
|
|
402
564
|
gap: 12,
|
|
403
565
|
},
|
|
566
|
+
sliderSection: {
|
|
567
|
+
marginTop: 16,
|
|
568
|
+
},
|
|
404
569
|
summaryCard: {
|
|
405
|
-
marginTop:
|
|
570
|
+
marginTop: 12,
|
|
406
571
|
borderRadius: 16,
|
|
407
572
|
borderWidth: 1,
|
|
408
573
|
overflow: 'hidden',
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import React, { useRef, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
PanResponder,
|
|
7
|
+
Dimensions,
|
|
8
|
+
Platform,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useDubsTheme } from '../theme';
|
|
11
|
+
|
|
12
|
+
const THUMB_SIZE = 32;
|
|
13
|
+
const TRACK_HEIGHT = 6;
|
|
14
|
+
const TICK_INTERVAL = 0.01; // haptic every 0.01 SOL
|
|
15
|
+
|
|
16
|
+
export interface SolSliderProps {
|
|
17
|
+
/** Current value in SOL */
|
|
18
|
+
value: number;
|
|
19
|
+
/** Min SOL (default 0.01) */
|
|
20
|
+
min?: number;
|
|
21
|
+
/** Max SOL (default 5) */
|
|
22
|
+
max?: number;
|
|
23
|
+
/** Step size in SOL (default 0.01) */
|
|
24
|
+
step?: number;
|
|
25
|
+
/** Accent color for filled track + thumb glow */
|
|
26
|
+
accentColor?: string;
|
|
27
|
+
/** Called on every drag frame */
|
|
28
|
+
onValueChange: (value: number) => void;
|
|
29
|
+
/** Called when user lifts finger */
|
|
30
|
+
onSlidingComplete?: (value: number) => void;
|
|
31
|
+
/** Called on each tick for haptics/sound */
|
|
32
|
+
onTick?: (value: number) => void;
|
|
33
|
+
/** Disabled state */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SolSlider({
|
|
38
|
+
value,
|
|
39
|
+
min = 0.01,
|
|
40
|
+
max = 5,
|
|
41
|
+
step = 0.01,
|
|
42
|
+
accentColor,
|
|
43
|
+
onValueChange,
|
|
44
|
+
onSlidingComplete,
|
|
45
|
+
onTick,
|
|
46
|
+
disabled = false,
|
|
47
|
+
}: SolSliderProps) {
|
|
48
|
+
const t = useDubsTheme();
|
|
49
|
+
const accent = accentColor || t.accent;
|
|
50
|
+
const trackRef = useRef<View>(null);
|
|
51
|
+
const trackWidth = useRef(0);
|
|
52
|
+
const lastTickValue = useRef(value);
|
|
53
|
+
|
|
54
|
+
const clamp = (v: number) => {
|
|
55
|
+
const stepped = Math.round(v / step) * step;
|
|
56
|
+
return Math.max(min, Math.min(max, parseFloat(stepped.toFixed(4))));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const valueToPosition = (v: number) => {
|
|
60
|
+
const ratio = (v - min) / (max - min);
|
|
61
|
+
return ratio * trackWidth.current;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const positionToValue = (x: number) => {
|
|
65
|
+
const ratio = Math.max(0, Math.min(1, x / trackWidth.current));
|
|
66
|
+
return clamp(min + ratio * (max - min));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const panResponder = useRef(
|
|
70
|
+
PanResponder.create({
|
|
71
|
+
onStartShouldSetPanResponder: () => !disabled,
|
|
72
|
+
onMoveShouldSetPanResponder: () => !disabled,
|
|
73
|
+
onPanResponderGrant: (_, gestureState) => {
|
|
74
|
+
lastTickValue.current = value;
|
|
75
|
+
},
|
|
76
|
+
onPanResponderMove: (evt, gestureState) => {
|
|
77
|
+
// Calculate position relative to track
|
|
78
|
+
const touchX = gestureState.moveX;
|
|
79
|
+
trackRef.current?.measureInWindow((trackX) => {
|
|
80
|
+
const relX = touchX - trackX;
|
|
81
|
+
const newVal = positionToValue(relX);
|
|
82
|
+
|
|
83
|
+
// Fire tick if we crossed a tick boundary
|
|
84
|
+
const tickDelta = Math.abs(newVal - lastTickValue.current);
|
|
85
|
+
if (tickDelta >= TICK_INTERVAL) {
|
|
86
|
+
lastTickValue.current = newVal;
|
|
87
|
+
onTick?.(newVal);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onValueChange(newVal);
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
onPanResponderRelease: () => {
|
|
94
|
+
onSlidingComplete?.(value);
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
).current;
|
|
98
|
+
|
|
99
|
+
const ratio = (value - min) / (max - min);
|
|
100
|
+
const filledWidth = `${ratio * 100}%`;
|
|
101
|
+
|
|
102
|
+
// Snap points for visual markers
|
|
103
|
+
const markers = [min, max * 0.25, max * 0.5, max * 0.75, max];
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<View style={styles.container}>
|
|
107
|
+
{/* Value display */}
|
|
108
|
+
<View style={styles.valueRow}>
|
|
109
|
+
<Text style={[styles.valueText, { color: accent }]}>
|
|
110
|
+
{parseFloat(value.toFixed(4))}
|
|
111
|
+
</Text>
|
|
112
|
+
<Text style={[styles.solLabel, { color: accent }]}>SOL</Text>
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
{/* Track */}
|
|
116
|
+
<View
|
|
117
|
+
ref={trackRef}
|
|
118
|
+
style={styles.trackContainer}
|
|
119
|
+
onLayout={(e) => { trackWidth.current = e.nativeEvent.layout.width; }}
|
|
120
|
+
{...panResponder.panHandlers}
|
|
121
|
+
>
|
|
122
|
+
{/* Background track */}
|
|
123
|
+
<View style={[styles.track, { backgroundColor: '#2C2C2E' }]}>
|
|
124
|
+
{/* Filled track */}
|
|
125
|
+
<View style={[styles.trackFilled, { width: filledWidth as any, backgroundColor: accent }]} />
|
|
126
|
+
</View>
|
|
127
|
+
|
|
128
|
+
{/* Tick markers */}
|
|
129
|
+
<View style={styles.markerRow}>
|
|
130
|
+
{markers.map((m, i) => {
|
|
131
|
+
const mRatio = (m - min) / (max - min);
|
|
132
|
+
return (
|
|
133
|
+
<View
|
|
134
|
+
key={i}
|
|
135
|
+
style={[
|
|
136
|
+
styles.marker,
|
|
137
|
+
{
|
|
138
|
+
left: `${mRatio * 100}%`,
|
|
139
|
+
backgroundColor: value >= m ? accent : '#3A3A3C',
|
|
140
|
+
},
|
|
141
|
+
]}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
{/* Thumb */}
|
|
148
|
+
<View
|
|
149
|
+
style={[
|
|
150
|
+
styles.thumb,
|
|
151
|
+
{
|
|
152
|
+
left: filledWidth as any,
|
|
153
|
+
backgroundColor: accent,
|
|
154
|
+
shadowColor: accent,
|
|
155
|
+
},
|
|
156
|
+
]}
|
|
157
|
+
>
|
|
158
|
+
<View style={styles.thumbInner} />
|
|
159
|
+
</View>
|
|
160
|
+
</View>
|
|
161
|
+
|
|
162
|
+
{/* Min/Max labels */}
|
|
163
|
+
<View style={styles.rangeRow}>
|
|
164
|
+
<Text style={styles.rangeText}>{min}</Text>
|
|
165
|
+
<Text style={styles.rangeText}>{max} SOL</Text>
|
|
166
|
+
</View>
|
|
167
|
+
</View>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const styles = StyleSheet.create({
|
|
172
|
+
container: {
|
|
173
|
+
paddingVertical: 8,
|
|
174
|
+
},
|
|
175
|
+
valueRow: {
|
|
176
|
+
flexDirection: 'row',
|
|
177
|
+
alignItems: 'baseline',
|
|
178
|
+
justifyContent: 'center',
|
|
179
|
+
marginBottom: 16,
|
|
180
|
+
gap: 4,
|
|
181
|
+
},
|
|
182
|
+
valueText: {
|
|
183
|
+
fontSize: 36,
|
|
184
|
+
fontWeight: '900',
|
|
185
|
+
letterSpacing: -1,
|
|
186
|
+
fontVariant: ['tabular-nums'],
|
|
187
|
+
},
|
|
188
|
+
solLabel: {
|
|
189
|
+
fontSize: 16,
|
|
190
|
+
fontWeight: '700',
|
|
191
|
+
opacity: 0.6,
|
|
192
|
+
},
|
|
193
|
+
trackContainer: {
|
|
194
|
+
height: THUMB_SIZE + 16,
|
|
195
|
+
justifyContent: 'center',
|
|
196
|
+
paddingHorizontal: THUMB_SIZE / 2,
|
|
197
|
+
},
|
|
198
|
+
track: {
|
|
199
|
+
height: TRACK_HEIGHT,
|
|
200
|
+
borderRadius: TRACK_HEIGHT / 2,
|
|
201
|
+
overflow: 'hidden',
|
|
202
|
+
},
|
|
203
|
+
trackFilled: {
|
|
204
|
+
height: '100%',
|
|
205
|
+
borderRadius: TRACK_HEIGHT / 2,
|
|
206
|
+
},
|
|
207
|
+
markerRow: {
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
left: THUMB_SIZE / 2,
|
|
210
|
+
right: THUMB_SIZE / 2,
|
|
211
|
+
height: TRACK_HEIGHT,
|
|
212
|
+
top: (THUMB_SIZE + 16 - TRACK_HEIGHT) / 2,
|
|
213
|
+
},
|
|
214
|
+
marker: {
|
|
215
|
+
position: 'absolute',
|
|
216
|
+
width: 3,
|
|
217
|
+
height: 12,
|
|
218
|
+
borderRadius: 1.5,
|
|
219
|
+
top: -3,
|
|
220
|
+
marginLeft: -1.5,
|
|
221
|
+
},
|
|
222
|
+
thumb: {
|
|
223
|
+
position: 'absolute',
|
|
224
|
+
width: THUMB_SIZE,
|
|
225
|
+
height: THUMB_SIZE,
|
|
226
|
+
borderRadius: THUMB_SIZE / 2,
|
|
227
|
+
top: 8,
|
|
228
|
+
marginLeft: 0,
|
|
229
|
+
alignItems: 'center',
|
|
230
|
+
justifyContent: 'center',
|
|
231
|
+
...Platform.select({
|
|
232
|
+
ios: {
|
|
233
|
+
shadowOffset: { width: 0, height: 0 },
|
|
234
|
+
shadowOpacity: 0.6,
|
|
235
|
+
shadowRadius: 10,
|
|
236
|
+
},
|
|
237
|
+
android: {
|
|
238
|
+
elevation: 8,
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
thumbInner: {
|
|
243
|
+
width: 12,
|
|
244
|
+
height: 12,
|
|
245
|
+
borderRadius: 6,
|
|
246
|
+
backgroundColor: '#FFFFFF',
|
|
247
|
+
},
|
|
248
|
+
rangeRow: {
|
|
249
|
+
flexDirection: 'row',
|
|
250
|
+
justifyContent: 'space-between',
|
|
251
|
+
paddingHorizontal: THUMB_SIZE / 2,
|
|
252
|
+
marginTop: 4,
|
|
253
|
+
},
|
|
254
|
+
rangeText: {
|
|
255
|
+
color: '#5A5A5E',
|
|
256
|
+
fontSize: 11,
|
|
257
|
+
fontWeight: '600',
|
|
258
|
+
},
|
|
259
|
+
});
|
package/src/ui/game/index.ts
CHANGED
|
@@ -20,3 +20,5 @@ export { EnterArcadePoolSheet } from './EnterArcadePoolSheet';
|
|
|
20
20
|
export type { EnterArcadePoolSheetProps } from './EnterArcadePoolSheet';
|
|
21
21
|
export { ArcadeLeaderboardSheet } from './ArcadeLeaderboardSheet';
|
|
22
22
|
export type { ArcadeLeaderboardSheetProps } from './ArcadeLeaderboardSheet';
|
|
23
|
+
export { SolSlider } from './SolSlider';
|
|
24
|
+
export type { SolSliderProps } from './SolSlider';
|
package/src/ui/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { useDubsTheme, mergeTheme } from './theme';
|
|
|
14
14
|
export type { DubsTheme } from './theme';
|
|
15
15
|
|
|
16
16
|
// Game widgets
|
|
17
|
-
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet } from './game';
|
|
17
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './game';
|
|
18
18
|
export type {
|
|
19
19
|
GamePosterProps,
|
|
20
20
|
LivePoolsCardProps,
|
|
@@ -27,4 +27,5 @@ export type {
|
|
|
27
27
|
ClaimButtonProps,
|
|
28
28
|
EnterArcadePoolSheetProps,
|
|
29
29
|
ArcadeLeaderboardSheetProps,
|
|
30
|
+
SolSliderProps,
|
|
30
31
|
} from './game';
|