@devrongx/games 0.4.6 → 0.4.7

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.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -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,60 @@ 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
+
125
180
  const betSummary = useMemo(
126
181
  () => config ? calcBetSummary(bets, config) : { selectedCount: 0, compoundMultiplier: 0, totalEntry: 0, baseReward: 0, compoundedReward: 0, remainingBalance: 0, riskPercent: 0, potentialBalance: 0 },
127
182
  [bets, config],
@@ -152,6 +207,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
152
207
  pickers[Number(mIdxStr)] = entry.optionIdx;
153
208
  }
154
209
  setExpandedPicker(pickers);
210
+ setUserFlowState("game");
155
211
  }, []);
156
212
 
157
213
  const handleOptionClick = useCallback((mIdx: number, oIdx: number) => {
@@ -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,
@@ -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