@devrongx/games 0.4.6 → 0.4.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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @devrongx/games — games/prematch-bets/PreMatchBetsPopup.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { useState, useCallback, useMemo } from "react";
|
|
4
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
|
5
5
|
import { Loader2 } from "lucide-react";
|
|
6
6
|
import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
7
7
|
import {
|
|
@@ -24,6 +24,7 @@ import { useTDPool, useTDPoolEntry, useTDLeaderboard } from "../../pools/hooks";
|
|
|
24
24
|
import { buildPMBConfig } from "../../pools/mapper";
|
|
25
25
|
import type { ITDMatch } from "../../matches/types";
|
|
26
26
|
import { joinTDPool, placeTDBets } from "../../pools/actions";
|
|
27
|
+
import { saveTDDraftBetsApi } from "../../pools/fetcher";
|
|
27
28
|
import type { ITDLeaderboardEntry } from "../../pools/types";
|
|
28
29
|
import { TDPoolStatus } from "../../pools/types";
|
|
29
30
|
import { calcRankPayout } from "./config";
|
|
@@ -122,6 +123,84 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
122
123
|
const [submitting, setSubmitting] = useState(false);
|
|
123
124
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
124
125
|
|
|
126
|
+
// ── Draft restore: when entry + config load, seed bets from draft_selections ─
|
|
127
|
+
const draftRestoredRef = useRef(false);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!poolId || !config || !entryData || draftRestoredRef.current) return;
|
|
130
|
+
const draft = entryData.entry.draft_selections;
|
|
131
|
+
if (!draft || draft.length === 0) return;
|
|
132
|
+
if (entryData.bets.length > 0) return; // real bets already placed — don't overwrite
|
|
133
|
+
const restored: IUserBets = {};
|
|
134
|
+
for (const d of draft) {
|
|
135
|
+
const mIdx = config.markets.findIndex((m) => m.backendChallengeId === d.challenge_id);
|
|
136
|
+
if (mIdx < 0) continue;
|
|
137
|
+
const optionIdx = config.markets[mIdx].options.findIndex(
|
|
138
|
+
(o) => o.label.toLowerCase().replace(/\s+/g, "_") === d.selected_option,
|
|
139
|
+
);
|
|
140
|
+
if (optionIdx < 0) continue;
|
|
141
|
+
restored[mIdx] = { optionIdx, amount: d.coin_amount, parlaySlot: null };
|
|
142
|
+
}
|
|
143
|
+
if (Object.keys(restored).length > 0) {
|
|
144
|
+
setBets(restored);
|
|
145
|
+
setUserFlowState("game");
|
|
146
|
+
}
|
|
147
|
+
draftRestoredRef.current = true;
|
|
148
|
+
}, [poolId, config, entryData]);
|
|
149
|
+
|
|
150
|
+
// ── Debounced draft save: persists bets to backend 1s after last change ───
|
|
151
|
+
const saveDraftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!poolId || Object.keys(bets).length === 0 || !config) return;
|
|
154
|
+
if (saveDraftTimerRef.current) clearTimeout(saveDraftTimerRef.current);
|
|
155
|
+
saveDraftTimerRef.current = setTimeout(async () => {
|
|
156
|
+
try {
|
|
157
|
+
const selections = config.markets
|
|
158
|
+
.map((m, mIdx) => {
|
|
159
|
+
const b = bets[mIdx];
|
|
160
|
+
if (!b || !m.backendChallengeId) return null;
|
|
161
|
+
const option = m.options[b.optionIdx];
|
|
162
|
+
if (!option) return null;
|
|
163
|
+
return {
|
|
164
|
+
challenge_id: m.backendChallengeId,
|
|
165
|
+
selected_option: option.label.toLowerCase().replace(/\s+/g, "_"),
|
|
166
|
+
coin_amount: b.amount,
|
|
167
|
+
};
|
|
168
|
+
})
|
|
169
|
+
.filter((b): b is NonNullable<typeof b> => b !== null);
|
|
170
|
+
if (selections.length > 0) {
|
|
171
|
+
await saveTDDraftBetsApi(poolId, selections);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Silent — draft save is best-effort
|
|
175
|
+
}
|
|
176
|
+
}, 1000);
|
|
177
|
+
return () => { if (saveDraftTimerRef.current) clearTimeout(saveDraftTimerRef.current); };
|
|
178
|
+
}, [bets, poolId, config]);
|
|
179
|
+
|
|
180
|
+
// Debounced save triggered from PreMatchQuestions on each option pick
|
|
181
|
+
const handleQuestionSelectionChange = useCallback((selections: IUserBets) => {
|
|
182
|
+
if (!poolId || !config) return;
|
|
183
|
+
if (saveDraftTimerRef.current) clearTimeout(saveDraftTimerRef.current);
|
|
184
|
+
saveDraftTimerRef.current = setTimeout(async () => {
|
|
185
|
+
try {
|
|
186
|
+
const draft = config.markets
|
|
187
|
+
.map((m, mIdx) => {
|
|
188
|
+
const b = selections[mIdx];
|
|
189
|
+
if (!b || !m.backendChallengeId) return null;
|
|
190
|
+
const option = m.options[b.optionIdx];
|
|
191
|
+
if (!option) return null;
|
|
192
|
+
return {
|
|
193
|
+
challenge_id: m.backendChallengeId,
|
|
194
|
+
selected_option: option.label.toLowerCase().replace(/\s+/g, "_"),
|
|
195
|
+
coin_amount: 0,
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((b): b is NonNullable<typeof b> => b !== null);
|
|
199
|
+
if (draft.length > 0) await saveTDDraftBetsApi(poolId, draft);
|
|
200
|
+
} catch { /* best-effort */ }
|
|
201
|
+
}, 1000);
|
|
202
|
+
}, [poolId, config]);
|
|
203
|
+
|
|
125
204
|
const betSummary = useMemo(
|
|
126
205
|
() => config ? calcBetSummary(bets, config) : { selectedCount: 0, compoundMultiplier: 0, totalEntry: 0, baseReward: 0, compoundedReward: 0, remainingBalance: 0, riskPercent: 0, potentialBalance: 0 },
|
|
127
206
|
[bets, config],
|
|
@@ -152,6 +231,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
152
231
|
pickers[Number(mIdxStr)] = entry.optionIdx;
|
|
153
232
|
}
|
|
154
233
|
setExpandedPicker(pickers);
|
|
234
|
+
setUserFlowState("game");
|
|
155
235
|
}, []);
|
|
156
236
|
|
|
157
237
|
const handleOptionClick = useCallback((mIdx: number, oIdx: number) => {
|
|
@@ -272,7 +352,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
272
352
|
)}
|
|
273
353
|
|
|
274
354
|
{activeView === "questions" && (
|
|
275
|
-
<PreMatchQuestions config={config} onComplete={handleQuestionsComplete} />
|
|
355
|
+
<PreMatchQuestions config={config} onComplete={handleQuestionsComplete} onSelectionChange={handleQuestionSelectionChange} />
|
|
276
356
|
)}
|
|
277
357
|
|
|
278
358
|
{activeView === "game" && (
|
|
@@ -11,6 +11,7 @@ import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
|
11
11
|
interface PreMatchQuestionsProps {
|
|
12
12
|
config: IChallengeConfig;
|
|
13
13
|
onComplete: (selections: IUserBets) => void;
|
|
14
|
+
onSelectionChange?: (selections: IUserBets) => void;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
// ── Smooth typewriter — constant speed, clean and simple ──
|
|
@@ -263,7 +264,7 @@ const QuestionSlide = ({
|
|
|
263
264
|
};
|
|
264
265
|
|
|
265
266
|
// ── Main component ──
|
|
266
|
-
export const PreMatchQuestions = ({ config, onComplete }: PreMatchQuestionsProps) => {
|
|
267
|
+
export const PreMatchQuestions = ({ config, onComplete, onSelectionChange }: PreMatchQuestionsProps) => {
|
|
267
268
|
const goTo = useGamePopupStore(s => s.goTo);
|
|
268
269
|
|
|
269
270
|
// -1 = start slide, 0..N-1 = question slides
|
|
@@ -283,19 +284,21 @@ export const PreMatchQuestions = ({ config, onComplete }: PreMatchQuestionsProps
|
|
|
283
284
|
}, []);
|
|
284
285
|
|
|
285
286
|
const handleOptionSelect = useCallback((mIdx: number, optionIdx: number) => {
|
|
287
|
+
const isDeselect = selections[mIdx]?.optionIdx === optionIdx;
|
|
288
|
+
|
|
286
289
|
setSelections(prev => {
|
|
287
|
-
|
|
288
|
-
if (prev[mIdx]?.optionIdx === optionIdx) {
|
|
290
|
+
if (isDeselect) {
|
|
289
291
|
const next = { ...prev };
|
|
290
292
|
delete next[mIdx];
|
|
293
|
+
onSelectionChange?.(next);
|
|
291
294
|
return next;
|
|
292
295
|
}
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
const next = { ...prev, [mIdx]: { optionIdx, amount: 0, parlaySlot: null } };
|
|
297
|
+
onSelectionChange?.(next);
|
|
298
|
+
return next;
|
|
295
299
|
});
|
|
296
300
|
|
|
297
301
|
// Auto-advance after a brief pause (only if selecting, not deselecting)
|
|
298
|
-
const isDeselect = selections[mIdx]?.optionIdx === optionIdx;
|
|
299
302
|
if (!isDeselect) {
|
|
300
303
|
setTimeout(() => {
|
|
301
304
|
if (mIdx < totalQuestions - 1) {
|
|
@@ -311,7 +314,7 @@ export const PreMatchQuestions = ({ config, onComplete }: PreMatchQuestionsProps
|
|
|
311
314
|
}
|
|
312
315
|
}, 500);
|
|
313
316
|
}
|
|
314
|
-
}, [config.markets, selections, totalQuestions, onComplete, goTo]);
|
|
317
|
+
}, [config.markets, selections, totalQuestions, onComplete, onSelectionChange, goTo]);
|
|
315
318
|
|
|
316
319
|
const seekNext = useCallback(() => {
|
|
317
320
|
if (step === -1) {
|
package/src/pools/fetcher.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ITDMyEntryResponse,
|
|
10
10
|
ITDLeaderboardResponse,
|
|
11
11
|
ITDBetInput,
|
|
12
|
+
ITDDraftSelection,
|
|
12
13
|
} from "./types";
|
|
13
14
|
|
|
14
15
|
// Shared fetch helper — adds auth header if token exists
|
|
@@ -78,6 +79,17 @@ export async function joinTDPoolApi(poolId: number): Promise<{ id: number }> {
|
|
|
78
79
|
return tdFetch<{ id: number }>(`/api/pools/${poolId}/join`, { method: "POST", body: "{}" });
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// PUT /api/pools/:id/draft-bets
|
|
83
|
+
export async function saveTDDraftBetsApi(
|
|
84
|
+
poolId: number,
|
|
85
|
+
selections: ITDDraftSelection[],
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
await tdFetch<unknown>(`/api/pools/${poolId}/draft-bets`, {
|
|
88
|
+
method: "PUT",
|
|
89
|
+
body: JSON.stringify({ selections }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
// POST /api/pools/:id/bets
|
|
82
94
|
export async function placeTDBetsApi(
|
|
83
95
|
poolId: number,
|
package/src/pools/types.ts
CHANGED
|
@@ -163,6 +163,12 @@ export interface ITDLeaderboardResponse {
|
|
|
163
163
|
|
|
164
164
|
// ─── Pool entry (GET /api/pools/:id/my-entry) ─────────────────────────────────
|
|
165
165
|
|
|
166
|
+
export interface ITDDraftSelection {
|
|
167
|
+
challenge_id: number;
|
|
168
|
+
selected_option: string;
|
|
169
|
+
coin_amount: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
166
172
|
export interface ITDPoolEntryRecord {
|
|
167
173
|
id: number;
|
|
168
174
|
pool_id: number;
|
|
@@ -178,6 +184,7 @@ export interface ITDPoolEntryRecord {
|
|
|
178
184
|
questions_left: number;
|
|
179
185
|
questions_total: number;
|
|
180
186
|
active_parlays: number;
|
|
187
|
+
draft_selections: ITDDraftSelection[] | null;
|
|
181
188
|
created_at: string;
|
|
182
189
|
}
|
|
183
190
|
|