@dubsdotapp/expo 0.2.29 → 0.2.30
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 +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +396 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +405 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +2 -0
- package/src/hooks/useCreateCustomGame.ts +2 -0
- package/src/index.ts +2 -1
- package/src/types.ts +5 -0
- package/src/ui/game/CreateCustomGameSheet.tsx +4 -1
- package/src/ui/game/JoinGameSheet.tsx +470 -0
- package/src/ui/game/index.ts +2 -0
- package/src/ui/index.ts +2 -1
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -215,6 +215,7 @@ export class DubsClient {
|
|
|
215
215
|
gameAddress: res.gameAddress,
|
|
216
216
|
transaction: res.transaction,
|
|
217
217
|
lockTimestamp: res.lockTimestamp,
|
|
218
|
+
externalGameId: res.externalGameId ?? null,
|
|
218
219
|
};
|
|
219
220
|
}
|
|
220
221
|
|
|
@@ -267,6 +268,7 @@ export class DubsClient {
|
|
|
267
268
|
const qs = new URLSearchParams();
|
|
268
269
|
if (params?.wallet) qs.set('wallet', params.wallet);
|
|
269
270
|
if (params?.status) qs.set('status', params.status);
|
|
271
|
+
if (params?.externalGameId) qs.set('externalGameId', params.externalGameId);
|
|
270
272
|
if (params?.limit != null) qs.set('limit', String(params.limit));
|
|
271
273
|
if (params?.offset != null) qs.set('offset', String(params.offset));
|
|
272
274
|
const query = qs.toString();
|
|
@@ -9,6 +9,7 @@ export interface CreateCustomGameMutationResult {
|
|
|
9
9
|
signature: string;
|
|
10
10
|
explorerUrl: string;
|
|
11
11
|
buyIn: number;
|
|
12
|
+
externalGameId?: string | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function useCreateCustomGame() {
|
|
@@ -65,6 +66,7 @@ export function useCreateCustomGame() {
|
|
|
65
66
|
signature,
|
|
66
67
|
explorerUrl,
|
|
67
68
|
buyIn: params.wagerAmount,
|
|
69
|
+
externalGameId: createResult.externalGameId ?? null,
|
|
68
70
|
};
|
|
69
71
|
|
|
70
72
|
setData(result);
|
package/src/index.ts
CHANGED
|
@@ -95,7 +95,7 @@ export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsT
|
|
|
95
95
|
export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, UserProfileCardProps, SettingsSheetProps, DubsTheme } from './ui';
|
|
96
96
|
|
|
97
97
|
// Game widgets
|
|
98
|
-
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet } from './ui';
|
|
98
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet } from './ui';
|
|
99
99
|
export type {
|
|
100
100
|
GamePosterProps,
|
|
101
101
|
LivePoolsCardProps,
|
|
@@ -103,6 +103,7 @@ export type {
|
|
|
103
103
|
PlayersCardProps,
|
|
104
104
|
JoinGameButtonProps,
|
|
105
105
|
CreateCustomGameSheetProps,
|
|
106
|
+
JoinGameSheetProps,
|
|
106
107
|
} from './ui';
|
|
107
108
|
|
|
108
109
|
// Utils
|
package/src/types.ts
CHANGED
|
@@ -114,6 +114,7 @@ export interface CreateCustomGameParams {
|
|
|
114
114
|
title?: string;
|
|
115
115
|
maxPlayers?: number;
|
|
116
116
|
metadata?: Record<string, unknown>;
|
|
117
|
+
externalGameId?: string;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
export interface CreateCustomGameResult {
|
|
@@ -121,6 +122,7 @@ export interface CreateCustomGameResult {
|
|
|
121
122
|
gameAddress: string;
|
|
122
123
|
transaction: string;
|
|
123
124
|
lockTimestamp: number;
|
|
125
|
+
externalGameId?: string | null;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// ── Join Game ──
|
|
@@ -203,6 +205,7 @@ export interface GameDetail {
|
|
|
203
205
|
drawPool: number;
|
|
204
206
|
totalPool: number;
|
|
205
207
|
media: GameMedia;
|
|
208
|
+
externalGameId?: string | null;
|
|
206
209
|
createdAt: string;
|
|
207
210
|
updatedAt: string;
|
|
208
211
|
}
|
|
@@ -226,6 +229,7 @@ export interface GameListItem {
|
|
|
226
229
|
league: string | null;
|
|
227
230
|
lockTimestamp: number | null;
|
|
228
231
|
createdAt: string;
|
|
232
|
+
externalGameId?: string | null;
|
|
229
233
|
opponents: GameListOpponent[];
|
|
230
234
|
media: GameMedia;
|
|
231
235
|
}
|
|
@@ -233,6 +237,7 @@ export interface GameListItem {
|
|
|
233
237
|
export interface GetGamesParams {
|
|
234
238
|
wallet?: string;
|
|
235
239
|
status?: 'open' | 'locked' | 'resolved';
|
|
240
|
+
externalGameId?: string;
|
|
236
241
|
limit?: number;
|
|
237
242
|
offset?: number;
|
|
238
243
|
}
|
|
@@ -25,6 +25,7 @@ export interface CreateCustomGameSheetProps {
|
|
|
25
25
|
presetAmounts?: number[];
|
|
26
26
|
defaultAmount?: number;
|
|
27
27
|
metadata?: Record<string, unknown>;
|
|
28
|
+
externalGameId?: string;
|
|
28
29
|
onAmountChange?: (amount: number | null) => void;
|
|
29
30
|
onSuccess?: (result: CreateCustomGameMutationResult) => void;
|
|
30
31
|
onError?: (error: Error) => void;
|
|
@@ -46,6 +47,7 @@ export function CreateCustomGameSheet({
|
|
|
46
47
|
presetAmounts = [0.01, 0.1, 0.5, 1],
|
|
47
48
|
defaultAmount = 0.01,
|
|
48
49
|
metadata,
|
|
50
|
+
externalGameId,
|
|
49
51
|
onAmountChange,
|
|
50
52
|
onSuccess,
|
|
51
53
|
onError,
|
|
@@ -139,11 +141,12 @@ export function CreateCustomGameSheet({
|
|
|
139
141
|
title,
|
|
140
142
|
maxPlayers,
|
|
141
143
|
metadata,
|
|
144
|
+
externalGameId,
|
|
142
145
|
});
|
|
143
146
|
} catch {
|
|
144
147
|
// Error is already captured in mutation state
|
|
145
148
|
}
|
|
146
|
-
}, [effectiveAmount, wallet.publicKey, mutation.execute, title, maxPlayers, metadata]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
149
|
+
}, [effectiveAmount, wallet.publicKey, mutation.execute, title, maxPlayers, metadata, externalGameId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
147
150
|
|
|
148
151
|
const statusLabel = STATUS_LABELS[mutation.status] || '';
|
|
149
152
|
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
Modal,
|
|
8
|
+
Animated,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
KeyboardAvoidingView,
|
|
11
|
+
Platform,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { useDubsTheme } from '../theme';
|
|
14
|
+
import { useDubs } from '../../provider';
|
|
15
|
+
import { useJoinGame } from '../../hooks/useJoinGame';
|
|
16
|
+
import type { JoinGameMutationResult } from '../../hooks/useJoinGame';
|
|
17
|
+
import type { GameDetail } from '../../types';
|
|
18
|
+
|
|
19
|
+
export interface JoinGameSheetProps {
|
|
20
|
+
visible: boolean;
|
|
21
|
+
onDismiss: () => void;
|
|
22
|
+
game: GameDetail;
|
|
23
|
+
/** Optional Image component (expo-image, etc.) for team logos */
|
|
24
|
+
ImageComponent?: React.ComponentType<any>;
|
|
25
|
+
/** Custom short-name function for team labels */
|
|
26
|
+
shortName?: (name: string | null) => string;
|
|
27
|
+
/** Override team colors (default blue/red) */
|
|
28
|
+
homeColor?: string;
|
|
29
|
+
awayColor?: string;
|
|
30
|
+
/** Callbacks */
|
|
31
|
+
onSuccess?: (result: JoinGameMutationResult) => void;
|
|
32
|
+
onError?: (error: Error) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
36
|
+
building: 'Building transaction...',
|
|
37
|
+
signing: 'Approve in wallet...',
|
|
38
|
+
confirming: 'Confirming...',
|
|
39
|
+
success: 'Joined!',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const CUSTOM_GAME_MODE = 6;
|
|
43
|
+
|
|
44
|
+
export function JoinGameSheet({
|
|
45
|
+
visible,
|
|
46
|
+
onDismiss,
|
|
47
|
+
game,
|
|
48
|
+
ImageComponent,
|
|
49
|
+
shortName,
|
|
50
|
+
homeColor = '#3B82F6',
|
|
51
|
+
awayColor = '#EF4444',
|
|
52
|
+
onSuccess,
|
|
53
|
+
onError,
|
|
54
|
+
}: JoinGameSheetProps) {
|
|
55
|
+
const t = useDubsTheme();
|
|
56
|
+
const { wallet } = useDubs();
|
|
57
|
+
const mutation = useJoinGame();
|
|
58
|
+
|
|
59
|
+
const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
|
|
60
|
+
|
|
61
|
+
// For custom games the joiner is always away — no team selection needed.
|
|
62
|
+
// For sports/esports games the user picks a team.
|
|
63
|
+
const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
|
|
64
|
+
|
|
65
|
+
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
|
66
|
+
|
|
67
|
+
// Animate overlay on visibility change
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
Animated.timing(overlayOpacity, {
|
|
70
|
+
toValue: visible ? 1 : 0,
|
|
71
|
+
duration: 250,
|
|
72
|
+
useNativeDriver: true,
|
|
73
|
+
}).start();
|
|
74
|
+
}, [visible, overlayOpacity]);
|
|
75
|
+
|
|
76
|
+
// Reset state when sheet opens
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (visible) {
|
|
79
|
+
setSelectedTeam(isCustomGame ? 'away' : null);
|
|
80
|
+
mutation.reset();
|
|
81
|
+
}
|
|
82
|
+
}, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
83
|
+
|
|
84
|
+
// Auto-dismiss on success
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (mutation.status === 'success' && mutation.data) {
|
|
87
|
+
onSuccess?.(mutation.data);
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
onDismiss();
|
|
90
|
+
}, 1500);
|
|
91
|
+
return () => clearTimeout(timer);
|
|
92
|
+
}
|
|
93
|
+
}, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
94
|
+
|
|
95
|
+
// Report errors
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (mutation.status === 'error' && mutation.error) {
|
|
98
|
+
onError?.(mutation.error);
|
|
99
|
+
}
|
|
100
|
+
}, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
101
|
+
|
|
102
|
+
// --- Derived values ---
|
|
103
|
+
|
|
104
|
+
const opponents = game.opponents || [];
|
|
105
|
+
const bettors = game.bettors || [];
|
|
106
|
+
const totalPool = game.totalPool || 0;
|
|
107
|
+
const homePool = game.homePool || 0;
|
|
108
|
+
const awayPool = game.awayPool || 0;
|
|
109
|
+
const buyIn = game.buyIn;
|
|
110
|
+
|
|
111
|
+
const { homeOdds, awayOdds, homeBets, awayBets } = useMemo(() => ({
|
|
112
|
+
homeOdds: homePool > 0 ? (totalPool / homePool).toFixed(2) : '—',
|
|
113
|
+
awayOdds: awayPool > 0 ? (totalPool / awayPool).toFixed(2) : '—',
|
|
114
|
+
homeBets: bettors.filter(b => b.team === 'home').length,
|
|
115
|
+
awayBets: bettors.filter(b => b.team === 'away').length,
|
|
116
|
+
}), [totalPool, homePool, awayPool, bettors]);
|
|
117
|
+
|
|
118
|
+
const poolAfterJoin = totalPool + buyIn;
|
|
119
|
+
const selectedOdds = selectedTeam === 'home' ? homeOdds : selectedTeam === 'away' ? awayOdds : '—';
|
|
120
|
+
const potentialWinnings = selectedOdds !== '—' ? (parseFloat(selectedOdds) * buyIn).toFixed(4) : '—';
|
|
121
|
+
|
|
122
|
+
const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
|
|
123
|
+
const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
|
|
124
|
+
const selectedName = selectedTeam === 'home' ? homeName : selectedTeam === 'away' ? awayName : '—';
|
|
125
|
+
|
|
126
|
+
const alreadyJoined = useMemo(() => {
|
|
127
|
+
if (!wallet.publicKey) return false;
|
|
128
|
+
const addr = wallet.publicKey.toBase58();
|
|
129
|
+
return bettors.some(b => b.wallet === addr);
|
|
130
|
+
}, [bettors, wallet.publicKey]);
|
|
131
|
+
|
|
132
|
+
const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error';
|
|
133
|
+
const canJoin = selectedTeam !== null && !isMutating && mutation.status !== 'success' && !alreadyJoined;
|
|
134
|
+
|
|
135
|
+
const handleJoin = useCallback(async () => {
|
|
136
|
+
if (!selectedTeam || !wallet.publicKey) return;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await mutation.execute({
|
|
140
|
+
playerWallet: wallet.publicKey.toBase58(),
|
|
141
|
+
gameId: game.gameId,
|
|
142
|
+
teamChoice: selectedTeam,
|
|
143
|
+
amount: buyIn,
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
// Error is already captured in mutation state
|
|
147
|
+
}
|
|
148
|
+
}, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId, buyIn]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
149
|
+
|
|
150
|
+
const statusLabel = STATUS_LABELS[mutation.status] || '';
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Modal
|
|
154
|
+
visible={visible}
|
|
155
|
+
animationType="slide"
|
|
156
|
+
transparent
|
|
157
|
+
onRequestClose={onDismiss}
|
|
158
|
+
>
|
|
159
|
+
<Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
|
|
160
|
+
<TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
|
|
161
|
+
</Animated.View>
|
|
162
|
+
|
|
163
|
+
<KeyboardAvoidingView
|
|
164
|
+
style={styles.keyboardView}
|
|
165
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
166
|
+
>
|
|
167
|
+
<View style={styles.sheetPositioner}>
|
|
168
|
+
<View style={[styles.sheet, { backgroundColor: t.background }]}>
|
|
169
|
+
{/* Drag handle */}
|
|
170
|
+
<View style={styles.handleRow}>
|
|
171
|
+
<View style={[styles.handle, { backgroundColor: t.textMuted }]} />
|
|
172
|
+
</View>
|
|
173
|
+
|
|
174
|
+
{/* Header */}
|
|
175
|
+
<View style={styles.header}>
|
|
176
|
+
<Text style={[styles.headerTitle, { color: t.text }]}>Join Game</Text>
|
|
177
|
+
<TouchableOpacity onPress={onDismiss} activeOpacity={0.8}>
|
|
178
|
+
<Text style={[styles.closeButton, { color: t.textMuted }]}>{'\u2715'}</Text>
|
|
179
|
+
</TouchableOpacity>
|
|
180
|
+
</View>
|
|
181
|
+
|
|
182
|
+
{/* Team Selection — only for non-custom (sports/esports) games */}
|
|
183
|
+
{!isCustomGame && (
|
|
184
|
+
<View style={styles.section}>
|
|
185
|
+
<Text style={[styles.sectionLabel, { color: t.textSecondary }]}>Pick Your Side</Text>
|
|
186
|
+
<View style={styles.teamsRow}>
|
|
187
|
+
<TeamButton
|
|
188
|
+
name={homeName}
|
|
189
|
+
imageUrl={opponents[0]?.imageUrl}
|
|
190
|
+
odds={homeOdds}
|
|
191
|
+
bets={homeBets}
|
|
192
|
+
color={homeColor}
|
|
193
|
+
selected={selectedTeam === 'home'}
|
|
194
|
+
onPress={() => setSelectedTeam('home')}
|
|
195
|
+
ImageComponent={ImageComponent}
|
|
196
|
+
t={t}
|
|
197
|
+
/>
|
|
198
|
+
<TeamButton
|
|
199
|
+
name={awayName}
|
|
200
|
+
imageUrl={opponents[1]?.imageUrl}
|
|
201
|
+
odds={awayOdds}
|
|
202
|
+
bets={awayBets}
|
|
203
|
+
color={awayColor}
|
|
204
|
+
selected={selectedTeam === 'away'}
|
|
205
|
+
onPress={() => setSelectedTeam('away')}
|
|
206
|
+
ImageComponent={ImageComponent}
|
|
207
|
+
t={t}
|
|
208
|
+
/>
|
|
209
|
+
</View>
|
|
210
|
+
</View>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{/* Summary Card */}
|
|
214
|
+
<View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
|
|
215
|
+
<View style={styles.summaryRow}>
|
|
216
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Buy-in</Text>
|
|
217
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{buyIn} SOL</Text>
|
|
218
|
+
</View>
|
|
219
|
+
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
220
|
+
<View style={styles.summaryRow}>
|
|
221
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your side</Text>
|
|
222
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{selectedName}</Text>
|
|
223
|
+
</View>
|
|
224
|
+
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
225
|
+
<View style={styles.summaryRow}>
|
|
226
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
|
|
227
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{poolAfterJoin} SOL</Text>
|
|
228
|
+
</View>
|
|
229
|
+
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
230
|
+
<View style={styles.summaryRow}>
|
|
231
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Potential winnings</Text>
|
|
232
|
+
<Text style={[styles.summaryValue, { color: t.success }]}>
|
|
233
|
+
{potentialWinnings !== '—' ? `${potentialWinnings} SOL` : '—'}
|
|
234
|
+
</Text>
|
|
235
|
+
</View>
|
|
236
|
+
</View>
|
|
237
|
+
|
|
238
|
+
{/* Already Joined Notice */}
|
|
239
|
+
{alreadyJoined && (
|
|
240
|
+
<View style={[styles.errorBox, { backgroundColor: t.surface, borderColor: t.border }]}>
|
|
241
|
+
<Text style={[styles.errorText, { color: t.textMuted }]}>You've already joined this game.</Text>
|
|
242
|
+
</View>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Error Display */}
|
|
246
|
+
{mutation.error && (
|
|
247
|
+
<View style={[styles.errorBox, { backgroundColor: t.errorBg, borderColor: t.errorBorder }]}>
|
|
248
|
+
<Text style={[styles.errorText, { color: t.errorText }]}>{mutation.error.message}</Text>
|
|
249
|
+
</View>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{/* CTA Button */}
|
|
253
|
+
<TouchableOpacity
|
|
254
|
+
style={[
|
|
255
|
+
styles.ctaButton,
|
|
256
|
+
{ backgroundColor: canJoin ? t.accent : t.border },
|
|
257
|
+
]}
|
|
258
|
+
disabled={!canJoin}
|
|
259
|
+
onPress={handleJoin}
|
|
260
|
+
activeOpacity={0.8}
|
|
261
|
+
>
|
|
262
|
+
{isMutating ? (
|
|
263
|
+
<View style={styles.ctaLoading}>
|
|
264
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
265
|
+
<Text style={styles.ctaText}>{statusLabel}</Text>
|
|
266
|
+
</View>
|
|
267
|
+
) : mutation.status === 'success' ? (
|
|
268
|
+
<Text style={styles.ctaText}>{STATUS_LABELS.success}</Text>
|
|
269
|
+
) : (
|
|
270
|
+
<Text style={[styles.ctaText, !canJoin && { opacity: 0.5 }]}>
|
|
271
|
+
{alreadyJoined
|
|
272
|
+
? 'Already Joined'
|
|
273
|
+
: selectedTeam
|
|
274
|
+
? `Join Game \u2014 ${buyIn} SOL`
|
|
275
|
+
: 'Pick a side to join'}
|
|
276
|
+
</Text>
|
|
277
|
+
)}
|
|
278
|
+
</TouchableOpacity>
|
|
279
|
+
</View>
|
|
280
|
+
</View>
|
|
281
|
+
</KeyboardAvoidingView>
|
|
282
|
+
</Modal>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── TeamButton (internal, for sports/esports games) ──
|
|
287
|
+
|
|
288
|
+
function TeamButton({
|
|
289
|
+
name, imageUrl, odds, bets, color, selected, onPress, ImageComponent, t,
|
|
290
|
+
}: {
|
|
291
|
+
name: string; imageUrl?: string | null; odds: string; bets: number;
|
|
292
|
+
color: string; selected: boolean; onPress: () => void;
|
|
293
|
+
ImageComponent?: React.ComponentType<any>; t: any;
|
|
294
|
+
}) {
|
|
295
|
+
const [imgFailed, setImgFailed] = useState(false);
|
|
296
|
+
const Img = ImageComponent || require('react-native').Image;
|
|
297
|
+
const showImage = imageUrl && !imgFailed;
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<TouchableOpacity
|
|
301
|
+
style={[styles.teamOption, { borderColor: selected ? color : t.border, backgroundColor: selected ? color + '15' : t.background }]}
|
|
302
|
+
onPress={onPress}
|
|
303
|
+
activeOpacity={0.7}
|
|
304
|
+
>
|
|
305
|
+
{showImage ? (
|
|
306
|
+
<Img source={{ uri: imageUrl }} style={styles.teamLogo} resizeMode="contain" onError={() => setImgFailed(true)} />
|
|
307
|
+
) : (
|
|
308
|
+
<View style={[styles.teamLogo, styles.teamLogoPlaceholder]} />
|
|
309
|
+
)}
|
|
310
|
+
<Text style={[styles.teamName, { color: t.text }]} numberOfLines={1}>{name}</Text>
|
|
311
|
+
<Text style={[styles.teamOdds, { color }]}>{odds}x</Text>
|
|
312
|
+
<Text style={[styles.teamBets, { color: t.textMuted }]}>{bets} {bets === 1 ? 'bet' : 'bets'}</Text>
|
|
313
|
+
{selected && (
|
|
314
|
+
<View style={[styles.teamBadge, { backgroundColor: color }]}>
|
|
315
|
+
<Text style={styles.teamBadgeText}>Selected</Text>
|
|
316
|
+
</View>
|
|
317
|
+
)}
|
|
318
|
+
</TouchableOpacity>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const styles = StyleSheet.create({
|
|
323
|
+
overlay: {
|
|
324
|
+
...StyleSheet.absoluteFillObject,
|
|
325
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
326
|
+
},
|
|
327
|
+
overlayTap: {
|
|
328
|
+
flex: 1,
|
|
329
|
+
},
|
|
330
|
+
keyboardView: {
|
|
331
|
+
flex: 1,
|
|
332
|
+
justifyContent: 'flex-end',
|
|
333
|
+
},
|
|
334
|
+
sheetPositioner: {
|
|
335
|
+
justifyContent: 'flex-end',
|
|
336
|
+
},
|
|
337
|
+
sheet: {
|
|
338
|
+
borderTopLeftRadius: 24,
|
|
339
|
+
borderTopRightRadius: 24,
|
|
340
|
+
paddingHorizontal: 20,
|
|
341
|
+
paddingBottom: 40,
|
|
342
|
+
},
|
|
343
|
+
handleRow: {
|
|
344
|
+
alignItems: 'center',
|
|
345
|
+
paddingTop: 10,
|
|
346
|
+
paddingBottom: 8,
|
|
347
|
+
},
|
|
348
|
+
handle: {
|
|
349
|
+
width: 36,
|
|
350
|
+
height: 4,
|
|
351
|
+
borderRadius: 2,
|
|
352
|
+
opacity: 0.4,
|
|
353
|
+
},
|
|
354
|
+
header: {
|
|
355
|
+
flexDirection: 'row',
|
|
356
|
+
alignItems: 'center',
|
|
357
|
+
justifyContent: 'space-between',
|
|
358
|
+
paddingVertical: 12,
|
|
359
|
+
},
|
|
360
|
+
headerTitle: {
|
|
361
|
+
fontSize: 20,
|
|
362
|
+
fontWeight: '700',
|
|
363
|
+
},
|
|
364
|
+
closeButton: {
|
|
365
|
+
fontSize: 20,
|
|
366
|
+
padding: 4,
|
|
367
|
+
},
|
|
368
|
+
section: {
|
|
369
|
+
gap: 12,
|
|
370
|
+
paddingTop: 8,
|
|
371
|
+
},
|
|
372
|
+
sectionLabel: {
|
|
373
|
+
fontSize: 15,
|
|
374
|
+
fontWeight: '600',
|
|
375
|
+
},
|
|
376
|
+
teamsRow: {
|
|
377
|
+
flexDirection: 'row',
|
|
378
|
+
gap: 12,
|
|
379
|
+
},
|
|
380
|
+
summaryCard: {
|
|
381
|
+
marginTop: 20,
|
|
382
|
+
borderRadius: 16,
|
|
383
|
+
borderWidth: 1,
|
|
384
|
+
overflow: 'hidden',
|
|
385
|
+
},
|
|
386
|
+
summaryRow: {
|
|
387
|
+
flexDirection: 'row',
|
|
388
|
+
alignItems: 'center',
|
|
389
|
+
justifyContent: 'space-between',
|
|
390
|
+
paddingHorizontal: 16,
|
|
391
|
+
paddingVertical: 14,
|
|
392
|
+
},
|
|
393
|
+
summaryLabel: {
|
|
394
|
+
fontSize: 14,
|
|
395
|
+
},
|
|
396
|
+
summaryValue: {
|
|
397
|
+
fontSize: 15,
|
|
398
|
+
fontWeight: '700',
|
|
399
|
+
},
|
|
400
|
+
summarySep: {
|
|
401
|
+
height: 1,
|
|
402
|
+
marginHorizontal: 16,
|
|
403
|
+
},
|
|
404
|
+
errorBox: {
|
|
405
|
+
marginTop: 16,
|
|
406
|
+
borderRadius: 12,
|
|
407
|
+
borderWidth: 1,
|
|
408
|
+
padding: 12,
|
|
409
|
+
},
|
|
410
|
+
errorText: {
|
|
411
|
+
fontSize: 13,
|
|
412
|
+
fontWeight: '500',
|
|
413
|
+
},
|
|
414
|
+
ctaButton: {
|
|
415
|
+
marginTop: 20,
|
|
416
|
+
height: 56,
|
|
417
|
+
borderRadius: 14,
|
|
418
|
+
justifyContent: 'center',
|
|
419
|
+
alignItems: 'center',
|
|
420
|
+
},
|
|
421
|
+
ctaText: {
|
|
422
|
+
color: '#FFFFFF',
|
|
423
|
+
fontSize: 16,
|
|
424
|
+
fontWeight: '700',
|
|
425
|
+
},
|
|
426
|
+
ctaLoading: {
|
|
427
|
+
flexDirection: 'row',
|
|
428
|
+
alignItems: 'center',
|
|
429
|
+
gap: 10,
|
|
430
|
+
},
|
|
431
|
+
// Team button styles
|
|
432
|
+
teamOption: {
|
|
433
|
+
flex: 1,
|
|
434
|
+
borderWidth: 2,
|
|
435
|
+
borderRadius: 16,
|
|
436
|
+
padding: 16,
|
|
437
|
+
alignItems: 'center',
|
|
438
|
+
gap: 8,
|
|
439
|
+
},
|
|
440
|
+
teamLogo: {
|
|
441
|
+
width: 48,
|
|
442
|
+
height: 48,
|
|
443
|
+
borderRadius: 24,
|
|
444
|
+
},
|
|
445
|
+
teamLogoPlaceholder: {
|
|
446
|
+
backgroundColor: 'rgba(128,128,128,0.2)',
|
|
447
|
+
},
|
|
448
|
+
teamName: {
|
|
449
|
+
fontSize: 15,
|
|
450
|
+
fontWeight: '700',
|
|
451
|
+
},
|
|
452
|
+
teamOdds: {
|
|
453
|
+
fontSize: 20,
|
|
454
|
+
fontWeight: '800',
|
|
455
|
+
},
|
|
456
|
+
teamBets: {
|
|
457
|
+
fontSize: 12,
|
|
458
|
+
},
|
|
459
|
+
teamBadge: {
|
|
460
|
+
borderRadius: 8,
|
|
461
|
+
paddingHorizontal: 12,
|
|
462
|
+
paddingVertical: 4,
|
|
463
|
+
marginTop: 4,
|
|
464
|
+
},
|
|
465
|
+
teamBadgeText: {
|
|
466
|
+
color: '#FFF',
|
|
467
|
+
fontSize: 12,
|
|
468
|
+
fontWeight: '700',
|
|
469
|
+
},
|
|
470
|
+
});
|
package/src/ui/game/index.ts
CHANGED
|
@@ -10,3 +10,5 @@ export { JoinGameButton } from './JoinGameButton';
|
|
|
10
10
|
export type { JoinGameButtonProps } from './JoinGameButton';
|
|
11
11
|
export { CreateCustomGameSheet } from './CreateCustomGameSheet';
|
|
12
12
|
export type { CreateCustomGameSheetProps } from './CreateCustomGameSheet';
|
|
13
|
+
export { JoinGameSheet } from './JoinGameSheet';
|
|
14
|
+
export type { JoinGameSheetProps } from './JoinGameSheet';
|
package/src/ui/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { useDubsTheme, mergeTheme } from './theme';
|
|
|
10
10
|
export type { DubsTheme } from './theme';
|
|
11
11
|
|
|
12
12
|
// Game widgets
|
|
13
|
-
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet } from './game';
|
|
13
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet } from './game';
|
|
14
14
|
export type {
|
|
15
15
|
GamePosterProps,
|
|
16
16
|
LivePoolsCardProps,
|
|
@@ -18,4 +18,5 @@ export type {
|
|
|
18
18
|
PlayersCardProps,
|
|
19
19
|
JoinGameButtonProps,
|
|
20
20
|
CreateCustomGameSheetProps,
|
|
21
|
+
JoinGameSheetProps,
|
|
21
22
|
} from './game';
|