@devrongx/games 0.3.5 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.3.5",
3
+ "version": "0.4.1",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,149 @@
1
+ // @devrongx/games — games/prematch-bets/FullLeaderboard.tsx
2
+ // Full paginated leaderboard with sort tabs and pinned user rank.
3
+ "use client";
4
+
5
+ import { useState } from "react";
6
+ import { ArrowLeft, Loader2 } from "lucide-react";
7
+ import { IChallengeConfig } from "./config";
8
+ import { LeaderboardRow } from "./LeaderboardRow";
9
+ import { OUTFIT } from "./constants";
10
+ import { useTDLeaderboard } from "../../pools/hooks";
11
+
12
+ const PAGE_SIZE = 50;
13
+
14
+ const SORT_TABS = [
15
+ { key: "current_coins", label: "Score" },
16
+ { key: "total_risked", label: "Risk" },
17
+ { key: "max_possible", label: "Max" },
18
+ { key: "questions_left", label: "Left" },
19
+ ] as const;
20
+
21
+ interface FullLeaderboardProps {
22
+ poolId: number;
23
+ config: IChallengeConfig;
24
+ onBack: () => void;
25
+ }
26
+
27
+ export function FullLeaderboard({ poolId, config, onBack }: FullLeaderboardProps) {
28
+ const [sortBy, setSortBy] = useState<string>("current_coins");
29
+ const [page, setPage] = useState(0);
30
+
31
+ const { rankings, total, loading } = useTDLeaderboard(poolId, {
32
+ sort: sortBy,
33
+ limit: PAGE_SIZE,
34
+ offset: page * PAGE_SIZE,
35
+ pollMs: 15_000,
36
+ });
37
+
38
+ return (
39
+ <div className="w-full flex flex-col">
40
+ {/* Header */}
41
+ <div className="flex items-center gap-3 px-4 pt-4 pb-3" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
42
+ <button onClick={onBack} className="flex items-center justify-center w-7 h-7 rounded-lg" style={{ background: "rgba(255,255,255,0.06)" }}>
43
+ <ArrowLeft size={14} className="text-white/60" />
44
+ </button>
45
+ <div className="flex-1">
46
+ <p className="text-[14px] font-bold text-white" style={OUTFIT}>Leaderboard</p>
47
+ <p className="text-[10px] text-white/40" style={OUTFIT}>{total.toLocaleString()} players</p>
48
+ </div>
49
+ </div>
50
+
51
+ {/* Sort tabs */}
52
+ <div className="flex gap-1 px-4 py-2" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
53
+ {SORT_TABS.map((tab) => (
54
+ <button
55
+ key={tab.key}
56
+ onClick={() => { setSortBy(tab.key); setPage(0); }}
57
+ className="flex-1 py-1.5 rounded-lg text-[10px] font-semibold transition-all"
58
+ style={{
59
+ ...OUTFIT,
60
+ background: sortBy === tab.key ? "rgba(34,227,232,0.15)" : "rgba(255,255,255,0.04)",
61
+ color: sortBy === tab.key ? "#22E3E8" : "rgba(255,255,255,0.4)",
62
+ border: sortBy === tab.key ? "1px solid rgba(34,227,232,0.3)" : "1px solid transparent",
63
+ }}
64
+ >
65
+ {tab.label}
66
+ </button>
67
+ ))}
68
+ </div>
69
+
70
+ {/* Column headers */}
71
+ <div className="flex items-center gap-2 px-4 py-1.5" style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
72
+ <span className="w-[28px] text-right text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>#</span>
73
+ <span className="flex-1 text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>Player</span>
74
+ <span className="w-[52px] text-right text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>Score</span>
75
+ <span className="w-[52px] text-right text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>Payout</span>
76
+ </div>
77
+
78
+ {/* Rows */}
79
+ {loading ? (
80
+ <div className="flex items-center justify-center py-10">
81
+ <Loader2 size={18} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} />
82
+ </div>
83
+ ) : (
84
+ <div className="flex flex-col overflow-y-auto" style={{ maxHeight: "55vh" }}>
85
+ {rankings.map((r) => (
86
+ <LeaderboardRow
87
+ key={r.entry_id}
88
+ entry={{
89
+ wallet: r.partner_ext_id ?? `User #${r.user_id}`,
90
+ pts: r.current_coins,
91
+ payout: 0,
92
+ isYou: false,
93
+ rank: r.rank,
94
+ }}
95
+ rank={r.rank}
96
+ isLast={false}
97
+ />
98
+ ))}
99
+ {!loading && rankings.length === 0 && (
100
+ <div className="flex items-center justify-center py-10">
101
+ <p className="text-[11px] text-white/30" style={OUTFIT}>No entries yet</p>
102
+ </div>
103
+ )}
104
+ </div>
105
+ )}
106
+
107
+ {/* Pagination */}
108
+ <div className="flex items-center justify-center gap-3 px-4 py-3" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
109
+ <button
110
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
111
+ disabled={page === 0}
112
+ className="text-[10px] font-semibold px-3 py-1.5 rounded-lg disabled:opacity-30"
113
+ style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)", color: "rgba(255,255,255,0.6)" }}
114
+ >
115
+ ← Prev
116
+ </button>
117
+ <span className="text-[10px] text-white/40" style={OUTFIT}>
118
+ {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
119
+ </span>
120
+ <button
121
+ onClick={() => setPage((p) => p + 1)}
122
+ disabled={(page + 1) * PAGE_SIZE >= total}
123
+ className="text-[10px] font-semibold px-3 py-1.5 rounded-lg disabled:opacity-30"
124
+ style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)", color: "rgba(255,255,255,0.6)" }}
125
+ >
126
+ Next →
127
+ </button>
128
+ </div>
129
+
130
+ {/* Payout brackets */}
131
+ {config.rankBrackets.length > 0 && (
132
+ <div className="px-4 pb-4">
133
+ <p className="text-[9px] text-white/30 font-semibold uppercase tracking-wider mb-1.5" style={OUTFIT}>Payout Brackets</p>
134
+ <div className="flex flex-wrap gap-1">
135
+ {config.rankBrackets.map((b, i) => (
136
+ <span
137
+ key={i}
138
+ className="text-[8px] font-medium px-2 py-0.5 rounded-full"
139
+ style={{ background: "rgba(255,255,255,0.04)", color: "rgba(255,255,255,0.4)", border: "1px solid rgba(255,255,255,0.08)" }}
140
+ >
141
+ #{b.from}{b.to > b.from ? `–${b.to}` : ""}: {(b.poolPercent * 100).toFixed(0)}%
142
+ </span>
143
+ ))}
144
+ </div>
145
+ </div>
146
+ )}
147
+ </div>
148
+ );
149
+ }
@@ -2,96 +2,327 @@
2
2
  "use client";
3
3
 
4
4
  import { useState, useCallback, useMemo } from "react";
5
+ import { Loader2 } from "lucide-react";
5
6
  import { useGamePopupStore } from "../../core/gamePopupStore";
6
- import { CSK_VS_RCB_CHALLENGE, CSK_VS_RCB_POOL, IUserBets, calcBetSummary, buildPoolLeaderboard } from "./config";
7
+ import {
8
+ CSK_VS_RCB_CHALLENGE,
9
+ CSK_VS_RCB_POOL,
10
+ IUserBets,
11
+ calcBetSummary,
12
+ buildPoolLeaderboard,
13
+ ILeaderboardEntry,
14
+ } from "./config";
7
15
  import { GamePopupShell } from "../../core/GamePopupShell";
8
16
  import { PreMatchIntro } from "./PreMatchIntro";
9
17
  import { PreMatchQuestions } from "./PreMatchQuestions";
10
18
  import { PreMatchGame } from "./PreMatchGame";
19
+ import { PreMatchSubmitted } from "./PreMatchSubmitted";
20
+ import { PreMatchLive } from "./PreMatchLive";
21
+ import { PreMatchResults } from "./PreMatchResults";
22
+ import { FullLeaderboard } from "./FullLeaderboard";
23
+ import { useTDPool, useTDPoolEntry, useTDLeaderboard } from "../../pools/hooks";
24
+ import { useTDMatches } from "../../matches/useTDMatches";
25
+ import { buildPMBConfig } from "../../pools/mapper";
26
+ import { joinTDPool, placeTDBets } from "../../pools/actions";
27
+ import type { ITDLeaderboardEntry } from "../../pools/types";
28
+ import { TDPoolStatus } from "../../pools/types";
29
+ import { calcRankPayout } from "./config";
11
30
 
12
31
  const GAME_ID = "pre-match";
13
- const config = CSK_VS_RCB_CHALLENGE;
14
- const pool = CSK_VS_RCB_POOL;
15
32
 
16
- export const PreMatchBetsPopup = () => {
17
- const { activeView } = useGamePopupStore();
33
+ // ─── View logic ────────────────────────────────────────────────────────────────
18
34
 
19
- // Unified bet state
20
- const [bets, setBets] = useState<IUserBets>({});
35
+ type UserFlowState = "intro" | "questions" | "game";
36
+ type ActiveView = UserFlowState | "submitted" | "live" | "results";
21
37
 
22
- // Picker state: which market has its amount picker open, and for which option
23
- const [expandedPicker, setExpandedPicker] = useState<Record<number, number>>({});
38
+ function getActiveView(
39
+ poolStatus: number | undefined,
40
+ hasEntry: boolean,
41
+ userFlowState: UserFlowState,
42
+ ): ActiveView {
43
+ if (poolStatus === undefined) return userFlowState;
44
+ if (poolStatus === TDPoolStatus.COMPLETE || poolStatus === TDPoolStatus.CANCELLED) return "results";
45
+ if (poolStatus === TDPoolStatus.CLOSED || poolStatus === TDPoolStatus.RESOLVING) return hasEntry ? "live" : "results";
46
+ // Pool is open
47
+ if (hasEntry) return "submitted";
48
+ return userFlowState;
49
+ }
24
50
 
25
- // Compute summary and leaderboard from bets
26
- const betSummary = useMemo(() => calcBetSummary(bets, config), [bets]);
27
- const leaderboard = useMemo(
28
- () => buildPoolLeaderboard(pool, betSummary.potentialBalance, config),
29
- [betSummary.potentialBalance],
30
- );
51
+ // Build a mini leaderboard from real API data
52
+ function buildMiniLeaderboard(
53
+ rankings: ITDLeaderboardEntry[],
54
+ userEntryId: number | null,
55
+ ): ILeaderboardEntry[] {
56
+ const rows: ILeaderboardEntry[] = [];
57
+ const top = rankings.slice(0, 3);
58
+ top.forEach((r) => {
59
+ rows.push({
60
+ wallet: r.partner_ext_id ?? `User #${r.user_id}`,
61
+ pts: r.current_coins,
62
+ payout: 0,
63
+ isYou: r.entry_id === userEntryId,
64
+ rank: r.rank,
65
+ });
66
+ });
67
+ if (userEntryId !== null) {
68
+ const userIdx = rankings.findIndex((r) => r.entry_id === userEntryId);
69
+ if (userIdx >= 3) {
70
+ rows.push({ wallet: "", pts: 0, payout: 0, isYou: false, gapAbove: true });
71
+ if (userIdx > 3) {
72
+ const above = rankings[userIdx - 1];
73
+ rows.push({ wallet: above.partner_ext_id ?? `User #${above.user_id}`, pts: above.current_coins, payout: 0, isYou: false, rank: userIdx });
74
+ }
75
+ const me = rankings[userIdx];
76
+ rows.push({ wallet: me.partner_ext_id ?? "You", pts: me.current_coins, payout: 0, isYou: true, rank: userIdx + 1 });
77
+ if (userIdx < rankings.length - 1) {
78
+ const below = rankings[userIdx + 1];
79
+ rows.push({ wallet: below.partner_ext_id ?? `User #${below.user_id}`, pts: below.current_coins, payout: 0, isYou: false, rank: userIdx + 2 });
80
+ }
81
+ }
82
+ }
83
+ return rows;
84
+ }
85
+
86
+ // ─── Props ────────────────────────────────────────────────────────────────────
87
+
88
+ interface PreMatchBetsPopupProps {
89
+ /** When provided, fetches real data. When absent, uses hardcoded CSK_VS_RCB demo. */
90
+ poolId?: number;
91
+ matchId?: number;
92
+ }
93
+
94
+ // ─── Component ───────────────────────────────────────────────────────────────
95
+
96
+ export const PreMatchBetsPopup = ({ poolId, matchId }: PreMatchBetsPopupProps) => {
97
+ const { activeView: storeView } = useGamePopupStore();
98
+
99
+ // ── Real API data (only when poolId provided) ────────────────────────────
100
+ const { pool, loading: poolLoading, refetch: refetchPool } = useTDPool(poolId ?? 0);
101
+ const { data: entryData, refetch: refetchEntry } = useTDPoolEntry(poolId ?? 0);
102
+ const { rankings, refetch: refetchLB } = useTDLeaderboard(poolId ?? 0, { pollMs: poolId ? 30_000 : 0 });
103
+ const { matches } = useTDMatches(matchId !== undefined ? { tournamentId: undefined } : undefined);
104
+ const match = useMemo(() => matches.find((m) => m.id === matchId) ?? null, [matches, matchId]);
105
+
106
+ // ── Config: real or fallback ─────────────────────────────────────────────
107
+ const config = useMemo(() => {
108
+ if (poolId && pool && match) {
109
+ try { return buildPMBConfig(pool, match); } catch { /* fall through */ }
110
+ }
111
+ return poolId ? null : CSK_VS_RCB_CHALLENGE;
112
+ }, [poolId, pool, match]);
113
+
114
+ const fallbackPool = poolId ? null : CSK_VS_RCB_POOL;
115
+
116
+ // ── Bet state ─────────────────────────────────────────────────────────────
117
+ const [bets, setBets] = useState<IUserBets>({});
118
+ const [expandedPicker, setExpandedPicker] = useState<Record<number, number>>({});
119
+ const [userFlowState, setUserFlowState] = useState<UserFlowState>("intro");
120
+ const [showFullLeaderboard, setShowFullLeaderboard] = useState(false);
121
+ const [submitting, setSubmitting] = useState(false);
122
+ const [submitError, setSubmitError] = useState<string | null>(null);
123
+
124
+ const betSummary = useMemo(
125
+ () => config ? calcBetSummary(bets, config) : { selectedCount: 0, compoundMultiplier: 0, totalEntry: 0, compoundedReward: 0, remainingBalance: 0, riskPercent: 0, potentialBalance: 0, prizes: [] },
126
+ [bets, config],
127
+ );
128
+
129
+ const leaderboardRows = useMemo(() => {
130
+ if (poolId && rankings.length > 0) {
131
+ return buildMiniLeaderboard(rankings, entryData?.entry.id ?? null);
132
+ }
133
+ if (fallbackPool && config) {
134
+ return buildPoolLeaderboard(fallbackPool, betSummary.potentialBalance, config).rows;
135
+ }
136
+ return [];
137
+ }, [poolId, rankings, entryData, fallbackPool, config, betSummary.potentialBalance]);
138
+
139
+ // ── View routing ──────────────────────────────────────────────────────────
140
+ const hasEntry = !!entryData?.entry;
141
+ const poolStatus = poolId ? pool?.status : undefined;
142
+ const activeView: ActiveView = poolId
143
+ ? getActiveView(poolStatus, hasEntry, userFlowState)
144
+ : (storeView as UserFlowState) || "intro";
145
+
146
+ // ── Handlers ─────────────────────────────────────────────────────────────
147
+ const handleQuestionsComplete = useCallback((selections: IUserBets) => {
148
+ setBets(selections);
149
+ const pickers: Record<number, number> = {};
150
+ for (const [mIdxStr, entry] of Object.entries(selections)) {
151
+ pickers[Number(mIdxStr)] = entry.optionIdx;
152
+ }
153
+ setExpandedPicker(pickers);
154
+ }, []);
155
+
156
+ const handleOptionClick = useCallback((mIdx: number, oIdx: number) => {
157
+ setBets((prev) => {
158
+ const existing = prev[mIdx];
159
+ if (existing?.optionIdx === oIdx) {
160
+ const next = { ...prev };
161
+ delete next[mIdx];
162
+ setExpandedPicker((p) => { const n = { ...p }; delete n[mIdx]; return n; });
163
+ return next;
164
+ }
165
+ setExpandedPicker((p) => ({ ...p, [mIdx]: oIdx }));
166
+ return {
167
+ ...prev,
168
+ [mIdx]: { optionIdx: oIdx, amount: existing?.amount ?? 0, parlaySlot: existing?.parlaySlot ?? null },
169
+ };
170
+ });
171
+ }, []);
172
+
173
+ const handleAmountSelect = useCallback((mIdx: number, oIdx: number, amount: number) => {
174
+ setBets((prev) => ({
175
+ ...prev,
176
+ [mIdx]: { optionIdx: oIdx, amount, parlaySlot: prev[mIdx]?.parlaySlot ?? null },
177
+ }));
178
+ }, []);
179
+
180
+ const handleSubmitBets = useCallback(async () => {
181
+ if (!poolId || !config) return;
182
+ setSubmitting(true);
183
+ setSubmitError(null);
184
+ try {
185
+ // Build bets payload — only individual bets with amount > 0
186
+ const betsToSubmit = Object.entries(bets)
187
+ .filter(([, b]) => b.amount > 0 && b.parlaySlot === null)
188
+ .map(([mIdxStr, b]) => {
189
+ const market = config.markets[Number(mIdxStr)];
190
+ return {
191
+ challenge_id: market.backendChallengeId!,
192
+ selected_option: market.options[b.optionIdx].label.toLowerCase().replace(/\s+/g, "_"),
193
+ coin_amount: b.amount,
194
+ };
195
+ })
196
+ .filter((b) => b.challenge_id !== undefined);
31
197
 
32
- // Questions completion callback
33
- const handleQuestionsComplete = useCallback((selections: IUserBets) => {
34
- setBets(selections);
35
- // Open pickers for all selected markets
36
- const pickers: Record<number, number> = {};
37
- for (const [mIdxStr, entry] of Object.entries(selections)) {
38
- pickers[Number(mIdxStr)] = entry.optionIdx;
198
+ if (betsToSubmit.length === 0) return;
199
+
200
+ // Join pool if not already in
201
+ if (!hasEntry) {
202
+ await joinTDPool(poolId);
203
+ }
204
+
205
+ await placeTDBets(poolId, betsToSubmit);
206
+
207
+ // Refresh data
208
+ await Promise.all([refetchEntry(), refetchLB(), refetchPool()]);
209
+ } catch (err: unknown) {
210
+ setSubmitError(err instanceof Error ? err.message : "Failed to submit bets");
211
+ } finally {
212
+ setSubmitting(false);
213
+ }
214
+ }, [poolId, config, bets, hasEntry, refetchEntry, refetchLB, refetchPool]);
215
+
216
+ const handleAdjust = useCallback(async () => {
217
+ // Load server bets back into local state
218
+ if (entryData?.bets && config) {
219
+ const loaded: IUserBets = {};
220
+ for (const bet of entryData.bets) {
221
+ if (bet.parlay_id !== null) continue; // skip parlays for now
222
+ const mIdx = config.markets.findIndex((m) => m.backendChallengeId === bet.challenge_id);
223
+ if (mIdx >= 0) {
224
+ // Find option by matching the stored key to a label approximation
225
+ // (we store selected_option as the label key from the pool option)
226
+ const oIdx = config.markets[mIdx].options.findIndex((_, i) => i === 0); // fallback: first option
227
+ loaded[mIdx] = { optionIdx: oIdx, amount: bet.coin_amount, parlaySlot: null };
39
228
  }
40
- setExpandedPicker(pickers);
41
- }, []);
42
-
43
- // Option click: toggle selection + open/close picker
44
- const handleOptionClick = useCallback((mIdx: number, oIdx: number) => {
45
- setBets(prev => {
46
- const existing = prev[mIdx];
47
- if (existing?.optionIdx === oIdx) {
48
- // Deselect
49
- const next = { ...prev };
50
- delete next[mIdx];
51
- setExpandedPicker(p => { const n = { ...p }; delete n[mIdx]; return n; });
52
- return next;
53
- }
54
- // Select new option — open picker
55
- setExpandedPicker(p => ({ ...p, [mIdx]: oIdx }));
56
- return {
57
- ...prev,
58
- [mIdx]: { optionIdx: oIdx, amount: existing?.amount ?? 0, parlaySlot: existing?.parlaySlot ?? null },
59
- };
60
- });
61
- }, []);
62
-
63
- // Amount select from chip picker
64
- const handleAmountSelect = useCallback((mIdx: number, oIdx: number, amount: number) => {
65
- setBets(prev => ({
66
- ...prev,
67
- [mIdx]: { optionIdx: oIdx, amount, parlaySlot: prev[mIdx]?.parlaySlot ?? null },
68
- }));
69
- }, []);
229
+ }
230
+ setBets(loaded);
231
+ }
232
+ setUserFlowState("game");
233
+ }, [entryData, config]);
234
+
235
+ // ── Loading ────────────────────────────────────────────────────────────────
236
+ if (poolId && (poolLoading || (!config && !poolLoading))) {
237
+ return (
238
+ <GamePopupShell gameId={GAME_ID} title="Pre-Match Bets" hideHeaderViews={["intro"]}>
239
+ <div className="flex-1 flex items-center justify-center py-20">
240
+ <Loader2 size={24} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} />
241
+ </div>
242
+ </GamePopupShell>
243
+ );
244
+ }
245
+
246
+ if (!config) return null;
70
247
 
248
+ // ── Full leaderboard overlay ───────────────────────────────────────────────
249
+ if (showFullLeaderboard) {
71
250
  return (
72
- <GamePopupShell
73
- gameId={GAME_ID}
74
- title="Pre-Match Bets"
75
- hideHeaderViews={["intro"]}
76
- >
77
- {activeView === "intro" && (
78
- <PreMatchIntro config={config} />
79
- )}
80
- {activeView === "questions" && (
81
- <PreMatchQuestions config={config} onComplete={handleQuestionsComplete} />
82
- )}
83
- {activeView === "game" && (
84
- <PreMatchGame
85
- config={config}
86
- bets={bets}
87
- onBetsChange={setBets}
88
- expandedPicker={expandedPicker}
89
- onOptionClick={handleOptionClick}
90
- onAmountSelect={handleAmountSelect}
91
- betSummary={betSummary}
92
- leaderboardRows={leaderboard.rows}
93
- />
94
- )}
95
- </GamePopupShell>
251
+ <GamePopupShell gameId={GAME_ID} title="Leaderboard" hideHeaderViews={[]} >
252
+ <FullLeaderboard
253
+ poolId={poolId ?? 0}
254
+ config={config}
255
+ onBack={() => setShowFullLeaderboard(false)}
256
+ />
257
+ </GamePopupShell>
96
258
  );
259
+ }
260
+
261
+ return (
262
+ <GamePopupShell gameId={GAME_ID} title="Pre-Match Bets" hideHeaderViews={["intro"]}>
263
+ {submitError && (
264
+ <div className="mx-4 mt-2 px-3 py-2 rounded-lg text-[11px] font-medium" style={{ background: "rgba(239,68,68,0.15)", color: "#f87171" }}>
265
+ {submitError}
266
+ </div>
267
+ )}
268
+
269
+ {activeView === "intro" && (
270
+ <PreMatchIntro config={config} />
271
+ )}
272
+
273
+ {activeView === "questions" && (
274
+ <PreMatchQuestions config={config} onComplete={handleQuestionsComplete} />
275
+ )}
276
+
277
+ {activeView === "game" && (
278
+ <PreMatchGame
279
+ config={config}
280
+ bets={bets}
281
+ onBetsChange={setBets}
282
+ expandedPicker={expandedPicker}
283
+ onOptionClick={handleOptionClick}
284
+ onAmountSelect={handleAmountSelect}
285
+ betSummary={betSummary}
286
+ leaderboardRows={leaderboardRows}
287
+ onSubmit={poolId ? handleSubmitBets : undefined}
288
+ onViewLeaderboard={poolId ? () => setShowFullLeaderboard(true) : undefined}
289
+ submitting={submitting}
290
+ />
291
+ )}
292
+
293
+ {activeView === "submitted" && (
294
+ <PreMatchSubmitted
295
+ config={config}
296
+ poolId={poolId!}
297
+ entryData={entryData}
298
+ rankings={rankings}
299
+ onAdjust={handleAdjust}
300
+ onViewLeaderboard={() => setShowFullLeaderboard(true)}
301
+ />
302
+ )}
303
+
304
+ {activeView === "live" && (
305
+ <PreMatchLive
306
+ config={config}
307
+ poolId={poolId!}
308
+ pool={pool}
309
+ entryData={entryData}
310
+ rankings={rankings}
311
+ onRefresh={() => { refetchPool(); refetchEntry(); refetchLB(); }}
312
+ onViewFullLeaderboard={() => setShowFullLeaderboard(true)}
313
+ />
314
+ )}
315
+
316
+ {activeView === "results" && (
317
+ <PreMatchResults
318
+ config={config}
319
+ poolId={poolId ?? 0}
320
+ pool={pool ?? undefined}
321
+ entryData={entryData}
322
+ rankings={rankings}
323
+ onViewFullLeaderboard={() => setShowFullLeaderboard(true)}
324
+ />
325
+ )}
326
+ </GamePopupShell>
327
+ );
97
328
  };