@dubsdotapp/expo 0.5.11 → 0.5.13
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 +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +575 -340
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +540 -296
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/ui/game/CreateGameSheet.tsx +326 -0
- package/src/ui/game/JoinGameSheet.tsx +1 -74
- package/src/ui/game/TeamButton.tsx +85 -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,7 @@ 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, SolSlider } from './ui';
|
|
137
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, CreateGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './ui';
|
|
138
138
|
export type { SolSliderProps } from './ui';
|
|
139
139
|
export type {
|
|
140
140
|
GamePosterProps,
|
|
@@ -143,6 +143,7 @@ export type {
|
|
|
143
143
|
PlayersCardProps,
|
|
144
144
|
JoinGameButtonProps,
|
|
145
145
|
CreateCustomGameSheetProps,
|
|
146
|
+
CreateGameSheetProps,
|
|
146
147
|
JoinGameSheetProps,
|
|
147
148
|
ClaimPrizeSheetProps,
|
|
148
149
|
ClaimButtonProps,
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Image,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
ActivityIndicator,
|
|
8
|
+
Modal,
|
|
9
|
+
Animated,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
KeyboardAvoidingView,
|
|
12
|
+
Platform,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { useDubsTheme } from '../theme';
|
|
15
|
+
import { useDubs } from '../../provider';
|
|
16
|
+
import { useCreateGame } from '../../hooks/useCreateGame';
|
|
17
|
+
import type { CreateGameMutationResult } from '../../hooks/useCreateGame';
|
|
18
|
+
import type { UnifiedEvent } from '../../types';
|
|
19
|
+
import { SolSlider } from './SolSlider';
|
|
20
|
+
import { TeamButton } from './TeamButton';
|
|
21
|
+
|
|
22
|
+
export interface CreateGameSheetProps {
|
|
23
|
+
visible: boolean;
|
|
24
|
+
onDismiss: () => void;
|
|
25
|
+
event: UnifiedEvent;
|
|
26
|
+
/** Override team colors */
|
|
27
|
+
homeColor?: string;
|
|
28
|
+
awayColor?: string;
|
|
29
|
+
/** Callbacks */
|
|
30
|
+
onSuccess?: (result: CreateGameMutationResult) => void;
|
|
31
|
+
onError?: (error: Error) => void;
|
|
32
|
+
/** Called when the user taps a team card */
|
|
33
|
+
onTeamSelect?: (team: 'home' | 'away') => void;
|
|
34
|
+
/** Called on success (for sound/animation) */
|
|
35
|
+
onCreateSuccess?: (result: CreateGameMutationResult) => void;
|
|
36
|
+
/** Called on each slider tick */
|
|
37
|
+
onSliderTick?: (value: number) => void;
|
|
38
|
+
/** Max wager in SOL */
|
|
39
|
+
maxWager?: number;
|
|
40
|
+
/** Custom short-name function for team labels */
|
|
41
|
+
shortName?: (name: string | null) => string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
45
|
+
building: 'Building transaction...',
|
|
46
|
+
signing: 'Approve in wallet...',
|
|
47
|
+
confirming: 'Confirming...',
|
|
48
|
+
success: 'Game Created!',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function formatSol(n: number): string {
|
|
52
|
+
return parseFloat(n.toFixed(9)).toString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toPng(url: string | null | undefined): string | null {
|
|
56
|
+
if (!url) return null;
|
|
57
|
+
return url.replace('/svg?', '/png?');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function CreateGameSheet({
|
|
61
|
+
visible,
|
|
62
|
+
onDismiss,
|
|
63
|
+
event,
|
|
64
|
+
homeColor = '#3B82F6',
|
|
65
|
+
awayColor = '#EF4444',
|
|
66
|
+
onSuccess,
|
|
67
|
+
onError,
|
|
68
|
+
onTeamSelect,
|
|
69
|
+
onCreateSuccess,
|
|
70
|
+
onSliderTick,
|
|
71
|
+
maxWager = 1,
|
|
72
|
+
shortName,
|
|
73
|
+
}: CreateGameSheetProps) {
|
|
74
|
+
const t = useDubsTheme();
|
|
75
|
+
const { wallet } = useDubs();
|
|
76
|
+
const mutation = useCreateGame();
|
|
77
|
+
|
|
78
|
+
const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
|
|
79
|
+
const [wager, setWager] = useState(0.01);
|
|
80
|
+
const [showSuccess, setShowSuccess] = useState(false);
|
|
81
|
+
|
|
82
|
+
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
|
83
|
+
const successScale = useRef(new Animated.Value(0)).current;
|
|
84
|
+
const successOpacity = useRef(new Animated.Value(0)).current;
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
Animated.timing(overlayOpacity, {
|
|
88
|
+
toValue: visible ? 1 : 0,
|
|
89
|
+
duration: 250,
|
|
90
|
+
useNativeDriver: true,
|
|
91
|
+
}).start();
|
|
92
|
+
}, [visible]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (visible) {
|
|
96
|
+
setSelectedTeam(null);
|
|
97
|
+
setWager(0.01);
|
|
98
|
+
setShowSuccess(false);
|
|
99
|
+
successScale.setValue(0);
|
|
100
|
+
successOpacity.setValue(0);
|
|
101
|
+
mutation.reset();
|
|
102
|
+
}
|
|
103
|
+
}, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
104
|
+
|
|
105
|
+
// Handle success
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (mutation.status === 'success' && mutation.data) {
|
|
108
|
+
setShowSuccess(true);
|
|
109
|
+
onSuccess?.(mutation.data);
|
|
110
|
+
onCreateSuccess?.(mutation.data);
|
|
111
|
+
|
|
112
|
+
Animated.parallel([
|
|
113
|
+
Animated.spring(successScale, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }),
|
|
114
|
+
Animated.timing(successOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
115
|
+
]).start();
|
|
116
|
+
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
Animated.timing(successOpacity, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
|
|
119
|
+
onDismiss();
|
|
120
|
+
});
|
|
121
|
+
}, 2500);
|
|
122
|
+
return () => clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
}, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (mutation.status === 'error' && mutation.error) {
|
|
128
|
+
onError?.(mutation.error);
|
|
129
|
+
}
|
|
130
|
+
}, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
131
|
+
|
|
132
|
+
const opponents = event.opponents || [];
|
|
133
|
+
const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
|
|
134
|
+
const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
|
|
135
|
+
|
|
136
|
+
const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error';
|
|
137
|
+
const canCreate = selectedTeam !== null && !isMutating && mutation.status !== 'success';
|
|
138
|
+
|
|
139
|
+
const handleCreate = useCallback(async () => {
|
|
140
|
+
if (!selectedTeam || !wallet.publicKey) return;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await mutation.execute({
|
|
144
|
+
id: event.id,
|
|
145
|
+
playerWallet: wallet.publicKey.toBase58(),
|
|
146
|
+
teamChoice: selectedTeam,
|
|
147
|
+
wagerAmount: wager,
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
// Error captured in mutation state
|
|
151
|
+
}
|
|
152
|
+
}, [selectedTeam, wallet.publicKey, mutation.execute, event.id, wager]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
153
|
+
|
|
154
|
+
const statusLabel = STATUS_LABELS[mutation.status] || '';
|
|
155
|
+
|
|
156
|
+
// Countdown
|
|
157
|
+
const startTime = event.startTime ? new Date(event.startTime) : null;
|
|
158
|
+
const timeLabel = startTime
|
|
159
|
+
? startTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) +
|
|
160
|
+
' at ' +
|
|
161
|
+
startTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
|
|
162
|
+
: 'TBD';
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Modal visible={visible} animationType="slide" transparent onRequestClose={onDismiss}>
|
|
166
|
+
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
|
167
|
+
<TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
|
|
168
|
+
</Animated.View>
|
|
169
|
+
|
|
170
|
+
{/* Success overlay */}
|
|
171
|
+
{showSuccess && (
|
|
172
|
+
<View style={styles.successOverlay}>
|
|
173
|
+
<Animated.View style={[styles.successContent, { opacity: successOpacity, transform: [{ scale: successScale }] }]}>
|
|
174
|
+
<Text style={styles.successEmoji}>🎯</Text>
|
|
175
|
+
<Text style={styles.successTitle}>Game Created!</Text>
|
|
176
|
+
<Text style={styles.successSub}>
|
|
177
|
+
{formatSol(wager)} SOL on {selectedTeam === 'home' ? homeName : awayName}
|
|
178
|
+
</Text>
|
|
179
|
+
</Animated.View>
|
|
180
|
+
</View>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<KeyboardAvoidingView style={styles.keyboardView} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
184
|
+
<View style={styles.sheetPositioner}>
|
|
185
|
+
<View style={[styles.sheet, { backgroundColor: t.background }]}>
|
|
186
|
+
{/* Drag handle */}
|
|
187
|
+
<View style={styles.handleRow}>
|
|
188
|
+
<View style={[styles.handle, { backgroundColor: t.textMuted }]} />
|
|
189
|
+
</View>
|
|
190
|
+
|
|
191
|
+
{/* Header */}
|
|
192
|
+
<View style={styles.header}>
|
|
193
|
+
<View>
|
|
194
|
+
<Text style={[styles.headerTitle, { color: t.text }]}>Create Game</Text>
|
|
195
|
+
<Text style={[styles.headerSub, { color: t.textMuted }]}>{timeLabel}</Text>
|
|
196
|
+
</View>
|
|
197
|
+
<TouchableOpacity onPress={onDismiss} activeOpacity={0.8}>
|
|
198
|
+
<Text style={[styles.closeButton, { color: t.textMuted }]}>{'\u2715'}</Text>
|
|
199
|
+
</TouchableOpacity>
|
|
200
|
+
</View>
|
|
201
|
+
|
|
202
|
+
{/* Team Selection */}
|
|
203
|
+
<View style={styles.section}>
|
|
204
|
+
<Text style={[styles.sectionLabel, { color: t.textSecondary }]}>Pick Your Side</Text>
|
|
205
|
+
<View style={styles.teamsRow}>
|
|
206
|
+
<TeamButton
|
|
207
|
+
name={homeName}
|
|
208
|
+
imageUrl={opponents[0]?.imageUrl}
|
|
209
|
+
odds="—"
|
|
210
|
+
bets={0}
|
|
211
|
+
color={homeColor}
|
|
212
|
+
selected={selectedTeam === 'home'}
|
|
213
|
+
onPress={() => { setSelectedTeam('home'); onTeamSelect?.('home'); }}
|
|
214
|
+
t={t}
|
|
215
|
+
/>
|
|
216
|
+
<TeamButton
|
|
217
|
+
name={awayName}
|
|
218
|
+
imageUrl={opponents[1]?.imageUrl}
|
|
219
|
+
odds="—"
|
|
220
|
+
bets={0}
|
|
221
|
+
color={awayColor}
|
|
222
|
+
selected={selectedTeam === 'away'}
|
|
223
|
+
onPress={() => { setSelectedTeam('away'); onTeamSelect?.('away'); }}
|
|
224
|
+
t={t}
|
|
225
|
+
/>
|
|
226
|
+
</View>
|
|
227
|
+
</View>
|
|
228
|
+
|
|
229
|
+
{/* Summary */}
|
|
230
|
+
<View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
|
|
231
|
+
<View style={styles.summaryRow}>
|
|
232
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your wager</Text>
|
|
233
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(wager)} SOL</Text>
|
|
234
|
+
</View>
|
|
235
|
+
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
236
|
+
<View style={styles.summaryRow}>
|
|
237
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>You're the first</Text>
|
|
238
|
+
<Text style={[styles.summaryValue, { color: t.success }]}>Set the odds</Text>
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
|
|
242
|
+
{/* SOL Slider */}
|
|
243
|
+
{selectedTeam && (
|
|
244
|
+
<SolSlider
|
|
245
|
+
value={wager}
|
|
246
|
+
min={0.01}
|
|
247
|
+
max={maxWager}
|
|
248
|
+
step={0.01}
|
|
249
|
+
accentColor={selectedTeam === 'home' ? homeColor : awayColor}
|
|
250
|
+
onValueChange={setWager}
|
|
251
|
+
onTick={onSliderTick}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{/* Error */}
|
|
256
|
+
{mutation.error && (
|
|
257
|
+
<View style={[styles.errorBox, { backgroundColor: t.errorBg, borderColor: t.errorBorder }]}>
|
|
258
|
+
<Text style={[styles.errorText, { color: t.errorText }]}>{mutation.error.message}</Text>
|
|
259
|
+
</View>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{/* CTA */}
|
|
263
|
+
<TouchableOpacity
|
|
264
|
+
style={[styles.ctaButton, { backgroundColor: canCreate ? t.accent : t.border }]}
|
|
265
|
+
disabled={!canCreate}
|
|
266
|
+
onPress={handleCreate}
|
|
267
|
+
activeOpacity={0.8}
|
|
268
|
+
>
|
|
269
|
+
{isMutating ? (
|
|
270
|
+
<View style={styles.ctaLoading}>
|
|
271
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
272
|
+
<Text style={styles.ctaText}>{statusLabel}</Text>
|
|
273
|
+
</View>
|
|
274
|
+
) : mutation.status === 'success' ? (
|
|
275
|
+
<Text style={styles.ctaText}>Game Created!</Text>
|
|
276
|
+
) : (
|
|
277
|
+
<Text style={[styles.ctaText, !canCreate && { opacity: 0.5 }]}>
|
|
278
|
+
{selectedTeam
|
|
279
|
+
? `Create Game \u2014 ${formatSol(wager)} SOL`
|
|
280
|
+
: 'Pick a side to start'}
|
|
281
|
+
</Text>
|
|
282
|
+
)}
|
|
283
|
+
</TouchableOpacity>
|
|
284
|
+
</View>
|
|
285
|
+
</View>
|
|
286
|
+
</KeyboardAvoidingView>
|
|
287
|
+
</Modal>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const styles = StyleSheet.create({
|
|
292
|
+
overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' },
|
|
293
|
+
overlayTap: { flex: 1 },
|
|
294
|
+
keyboardView: { flex: 1, justifyContent: 'flex-end' },
|
|
295
|
+
sheetPositioner: { justifyContent: 'flex-end' },
|
|
296
|
+
sheet: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingHorizontal: 20, paddingBottom: 40 },
|
|
297
|
+
handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 8 },
|
|
298
|
+
handle: { width: 36, height: 4, borderRadius: 2, opacity: 0.4 },
|
|
299
|
+
header: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingVertical: 8 },
|
|
300
|
+
headerTitle: { fontSize: 20, fontWeight: '700' },
|
|
301
|
+
headerSub: { fontSize: 13, marginTop: 2 },
|
|
302
|
+
closeButton: { fontSize: 20, padding: 4 },
|
|
303
|
+
|
|
304
|
+
section: { gap: 10, paddingTop: 12 },
|
|
305
|
+
sectionLabel: { fontSize: 14, fontWeight: '600' },
|
|
306
|
+
teamsRow: { flexDirection: 'row', gap: 12 },
|
|
307
|
+
|
|
308
|
+
summaryCard: { marginTop: 12, borderRadius: 14, borderWidth: 1, overflow: 'hidden' },
|
|
309
|
+
summaryRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12 },
|
|
310
|
+
summaryLabel: { fontSize: 14 },
|
|
311
|
+
summaryValue: { fontSize: 15, fontWeight: '700' },
|
|
312
|
+
summarySep: { height: 1, marginHorizontal: 16 },
|
|
313
|
+
|
|
314
|
+
errorBox: { marginTop: 12, borderRadius: 12, borderWidth: 1, padding: 12 },
|
|
315
|
+
errorText: { fontSize: 13, fontWeight: '500' },
|
|
316
|
+
|
|
317
|
+
ctaButton: { marginTop: 16, height: 54, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
|
318
|
+
ctaText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' },
|
|
319
|
+
ctaLoading: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
320
|
+
|
|
321
|
+
successOverlay: { ...StyleSheet.absoluteFillObject, zIndex: 100, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.85)' },
|
|
322
|
+
successContent: { alignItems: 'center', gap: 12 },
|
|
323
|
+
successEmoji: { fontSize: 64 },
|
|
324
|
+
successTitle: { color: '#FFFFFF', fontSize: 28, fontWeight: '900' },
|
|
325
|
+
successSub: { color: '#8E8E93', fontSize: 16 },
|
|
326
|
+
});
|
|
@@ -17,6 +17,7 @@ import { useJoinGame } from '../../hooks/useJoinGame';
|
|
|
17
17
|
import type { JoinGameMutationResult } from '../../hooks/useJoinGame';
|
|
18
18
|
import type { GameDetail } from '../../types';
|
|
19
19
|
import { SolSlider } from './SolSlider';
|
|
20
|
+
import { TeamButton } from './TeamButton';
|
|
20
21
|
|
|
21
22
|
export interface JoinGameSheetProps {
|
|
22
23
|
visible: boolean;
|
|
@@ -412,41 +413,6 @@ export function JoinGameSheet({
|
|
|
412
413
|
);
|
|
413
414
|
}
|
|
414
415
|
|
|
415
|
-
// ── TeamButton (internal, for sports/esports games) ──
|
|
416
|
-
|
|
417
|
-
function TeamButton({
|
|
418
|
-
name, imageUrl, odds, bets, color, selected, onPress, ImageComponent, t,
|
|
419
|
-
}: {
|
|
420
|
-
name: string; imageUrl?: string | null; odds: string; bets: number;
|
|
421
|
-
color: string; selected: boolean; onPress: () => void;
|
|
422
|
-
ImageComponent?: React.ComponentType<any>; t: any;
|
|
423
|
-
}) {
|
|
424
|
-
const [imgFailed, setImgFailed] = useState(false);
|
|
425
|
-
const Img = ImageComponent || require('react-native').Image;
|
|
426
|
-
const showImage = imageUrl && !imgFailed;
|
|
427
|
-
|
|
428
|
-
return (
|
|
429
|
-
<TouchableOpacity
|
|
430
|
-
style={[styles.teamOption, { borderColor: selected ? color : t.border, backgroundColor: selected ? color + '15' : t.background }]}
|
|
431
|
-
onPress={onPress}
|
|
432
|
-
activeOpacity={0.7}
|
|
433
|
-
>
|
|
434
|
-
{showImage ? (
|
|
435
|
-
<Img source={{ uri: imageUrl }} style={styles.teamLogo} resizeMode="contain" onError={() => setImgFailed(true)} />
|
|
436
|
-
) : (
|
|
437
|
-
<View style={[styles.teamLogo, styles.teamLogoPlaceholder]} />
|
|
438
|
-
)}
|
|
439
|
-
<Text style={[styles.teamName, { color: t.text }]} numberOfLines={1}>{name}</Text>
|
|
440
|
-
<Text style={[styles.teamOdds, { color }]}>{odds}x</Text>
|
|
441
|
-
<Text style={[styles.teamBets, { color: t.textMuted }]}>{bets} {bets === 1 ? 'bet' : 'bets'}</Text>
|
|
442
|
-
{selected && (
|
|
443
|
-
<View style={[styles.teamBadge, { backgroundColor: color }]}>
|
|
444
|
-
<Text style={styles.teamBadgeText}>Selected</Text>
|
|
445
|
-
</View>
|
|
446
|
-
)}
|
|
447
|
-
</TouchableOpacity>
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
416
|
|
|
451
417
|
const styles = StyleSheet.create({
|
|
452
418
|
overlay: {
|
|
@@ -642,43 +608,4 @@ const styles = StyleSheet.create({
|
|
|
642
608
|
alignItems: 'center',
|
|
643
609
|
gap: 10,
|
|
644
610
|
},
|
|
645
|
-
// Team button styles
|
|
646
|
-
teamOption: {
|
|
647
|
-
flex: 1,
|
|
648
|
-
borderWidth: 2,
|
|
649
|
-
borderRadius: 16,
|
|
650
|
-
padding: 16,
|
|
651
|
-
alignItems: 'center',
|
|
652
|
-
gap: 8,
|
|
653
|
-
},
|
|
654
|
-
teamLogo: {
|
|
655
|
-
width: 48,
|
|
656
|
-
height: 48,
|
|
657
|
-
borderRadius: 24,
|
|
658
|
-
},
|
|
659
|
-
teamLogoPlaceholder: {
|
|
660
|
-
backgroundColor: 'rgba(128,128,128,0.2)',
|
|
661
|
-
},
|
|
662
|
-
teamName: {
|
|
663
|
-
fontSize: 15,
|
|
664
|
-
fontWeight: '700',
|
|
665
|
-
},
|
|
666
|
-
teamOdds: {
|
|
667
|
-
fontSize: 20,
|
|
668
|
-
fontWeight: '800',
|
|
669
|
-
},
|
|
670
|
-
teamBets: {
|
|
671
|
-
fontSize: 12,
|
|
672
|
-
},
|
|
673
|
-
teamBadge: {
|
|
674
|
-
borderRadius: 8,
|
|
675
|
-
paddingHorizontal: 12,
|
|
676
|
-
paddingVertical: 4,
|
|
677
|
-
marginTop: 4,
|
|
678
|
-
},
|
|
679
|
-
teamBadgeText: {
|
|
680
|
-
color: '#FFF',
|
|
681
|
-
fontSize: 12,
|
|
682
|
-
fontWeight: '700',
|
|
683
|
-
},
|
|
684
611
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface TeamButtonProps {
|
|
5
|
+
name: string;
|
|
6
|
+
imageUrl?: string | null;
|
|
7
|
+
odds: string;
|
|
8
|
+
bets: number;
|
|
9
|
+
color: string;
|
|
10
|
+
selected: boolean;
|
|
11
|
+
onPress: () => void;
|
|
12
|
+
ImageComponent?: React.ComponentType<any>;
|
|
13
|
+
t: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TeamButton({
|
|
17
|
+
name, imageUrl, odds, bets, color, selected, onPress, ImageComponent, t,
|
|
18
|
+
}: TeamButtonProps) {
|
|
19
|
+
const [imgFailed, setImgFailed] = useState(false);
|
|
20
|
+
const Img = ImageComponent || require('react-native').Image;
|
|
21
|
+
const showImage = imageUrl && !imgFailed;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<TouchableOpacity
|
|
25
|
+
style={[styles.teamOption, { borderColor: selected ? color : t.border, backgroundColor: selected ? color + '15' : t.background }]}
|
|
26
|
+
onPress={onPress}
|
|
27
|
+
activeOpacity={0.7}
|
|
28
|
+
>
|
|
29
|
+
{showImage ? (
|
|
30
|
+
<Img source={{ uri: imageUrl }} style={styles.teamLogo} resizeMode="contain" onError={() => setImgFailed(true)} />
|
|
31
|
+
) : (
|
|
32
|
+
<View style={[styles.teamLogo, styles.teamLogoPlaceholder]} />
|
|
33
|
+
)}
|
|
34
|
+
<Text style={[styles.teamName, { color: t.text }]} numberOfLines={1}>{name}</Text>
|
|
35
|
+
<Text style={[styles.teamOdds, { color }]}>{odds}x</Text>
|
|
36
|
+
<Text style={[styles.teamBets, { color: t.textMuted }]}>{bets} {bets === 1 ? 'bet' : 'bets'}</Text>
|
|
37
|
+
{selected && (
|
|
38
|
+
<View style={[styles.teamBadge, { backgroundColor: color }]}>
|
|
39
|
+
<Text style={styles.teamBadgeText}>Selected</Text>
|
|
40
|
+
</View>
|
|
41
|
+
)}
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const styles = StyleSheet.create({
|
|
47
|
+
teamOption: {
|
|
48
|
+
flex: 1,
|
|
49
|
+
borderWidth: 2,
|
|
50
|
+
borderRadius: 16,
|
|
51
|
+
padding: 16,
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
gap: 8,
|
|
54
|
+
},
|
|
55
|
+
teamLogo: {
|
|
56
|
+
width: 48,
|
|
57
|
+
height: 48,
|
|
58
|
+
borderRadius: 24,
|
|
59
|
+
},
|
|
60
|
+
teamLogoPlaceholder: {
|
|
61
|
+
backgroundColor: 'rgba(128,128,128,0.2)',
|
|
62
|
+
},
|
|
63
|
+
teamName: {
|
|
64
|
+
fontSize: 15,
|
|
65
|
+
fontWeight: '700',
|
|
66
|
+
},
|
|
67
|
+
teamOdds: {
|
|
68
|
+
fontSize: 20,
|
|
69
|
+
fontWeight: '800',
|
|
70
|
+
},
|
|
71
|
+
teamBets: {
|
|
72
|
+
fontSize: 12,
|
|
73
|
+
},
|
|
74
|
+
teamBadge: {
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
paddingHorizontal: 12,
|
|
77
|
+
paddingVertical: 4,
|
|
78
|
+
marginTop: 4,
|
|
79
|
+
},
|
|
80
|
+
teamBadgeText: {
|
|
81
|
+
color: '#FFF',
|
|
82
|
+
fontSize: 12,
|
|
83
|
+
fontWeight: '700',
|
|
84
|
+
},
|
|
85
|
+
});
|
package/src/ui/game/index.ts
CHANGED
|
@@ -22,3 +22,5 @@ export { ArcadeLeaderboardSheet } from './ArcadeLeaderboardSheet';
|
|
|
22
22
|
export type { ArcadeLeaderboardSheetProps } from './ArcadeLeaderboardSheet';
|
|
23
23
|
export { SolSlider } from './SolSlider';
|
|
24
24
|
export type { SolSliderProps } from './SolSlider';
|
|
25
|
+
export { CreateGameSheet } from './CreateGameSheet';
|
|
26
|
+
export type { CreateGameSheetProps } from './CreateGameSheet';
|
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, SolSlider } from './game';
|
|
17
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, CreateGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './game';
|
|
18
18
|
export type {
|
|
19
19
|
GamePosterProps,
|
|
20
20
|
LivePoolsCardProps,
|
|
@@ -28,4 +28,5 @@ export type {
|
|
|
28
28
|
EnterArcadePoolSheetProps,
|
|
29
29
|
ArcadeLeaderboardSheetProps,
|
|
30
30
|
SolSliderProps,
|
|
31
|
+
CreateGameSheetProps,
|
|
31
32
|
} from './game';
|