@devrongx/games 0.4.12 → 0.4.14
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
|
@@ -120,6 +120,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
120
120
|
const [showFullLeaderboard, setShowFullLeaderboard] = useState(false);
|
|
121
121
|
const [submitting, setSubmitting] = useState(false);
|
|
122
122
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
123
|
+
const [submittedBets, setSubmittedBets] = useState<IUserBets | null>(null);
|
|
123
124
|
|
|
124
125
|
// ── Real bets restore: when user has submitted bets, load them into game state ─
|
|
125
126
|
const realBetsRestoredRef = useRef(false);
|
|
@@ -141,6 +142,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
141
142
|
}
|
|
142
143
|
if (Object.keys(loaded).length === 0) return;
|
|
143
144
|
setBets(loaded);
|
|
145
|
+
setSubmittedBets(loaded);
|
|
144
146
|
setExpandedPicker(pickers);
|
|
145
147
|
setUserFlowState("game");
|
|
146
148
|
realBetsRestoredRef.current = true;
|
|
@@ -182,22 +184,22 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
182
184
|
|
|
183
185
|
const saveDraftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
184
186
|
|
|
185
|
-
//
|
|
186
|
-
const
|
|
187
|
+
// Unified debounced draft save — call with current bets (includes amounts)
|
|
188
|
+
const saveDraftDebounced = useCallback((currentBets: IUserBets) => {
|
|
187
189
|
if (!poolId || !config) return;
|
|
188
190
|
if (saveDraftTimerRef.current) clearTimeout(saveDraftTimerRef.current);
|
|
189
191
|
saveDraftTimerRef.current = setTimeout(async () => {
|
|
190
192
|
try {
|
|
191
193
|
const draft = config.markets
|
|
192
194
|
.map((m, mIdx) => {
|
|
193
|
-
const b =
|
|
195
|
+
const b = currentBets[mIdx];
|
|
194
196
|
if (!b || !m.backendChallengeId) return null;
|
|
195
197
|
const option = m.options[b.optionIdx];
|
|
196
198
|
if (!option) return null;
|
|
197
199
|
return {
|
|
198
200
|
challenge_id: m.backendChallengeId,
|
|
199
201
|
selected_option: option.label.toLowerCase().replace(/\s+/g, "_"),
|
|
200
|
-
coin_amount:
|
|
202
|
+
coin_amount: b.amount,
|
|
201
203
|
};
|
|
202
204
|
})
|
|
203
205
|
.filter((b): b is NonNullable<typeof b> => b !== null);
|
|
@@ -206,6 +208,11 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
206
208
|
}, 1000);
|
|
207
209
|
}, [poolId, config]);
|
|
208
210
|
|
|
211
|
+
// Called from PreMatchQuestions on each option pick
|
|
212
|
+
const handleQuestionSelectionChange = useCallback((selections: IUserBets) => {
|
|
213
|
+
saveDraftDebounced(selections);
|
|
214
|
+
}, [saveDraftDebounced]);
|
|
215
|
+
|
|
209
216
|
const betSummary = useMemo(
|
|
210
217
|
() => config ? calcBetSummary(bets, config) : { selectedCount: 0, compoundMultiplier: 0, totalEntry: 0, baseReward: 0, compoundedReward: 0, remainingBalance: 0, riskPercent: 0, potentialBalance: 0 },
|
|
211
218
|
[bets, config],
|
|
@@ -257,11 +264,12 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
257
264
|
}, []);
|
|
258
265
|
|
|
259
266
|
const handleAmountSelect = useCallback((mIdx: number, oIdx: number, amount: number) => {
|
|
260
|
-
setBets((prev) =>
|
|
261
|
-
...prev,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
setBets((prev) => {
|
|
268
|
+
const next = { ...prev, [mIdx]: { optionIdx: oIdx, amount, parlaySlot: prev[mIdx]?.parlaySlot ?? null } };
|
|
269
|
+
saveDraftDebounced(next);
|
|
270
|
+
return next;
|
|
271
|
+
});
|
|
272
|
+
}, [saveDraftDebounced]);
|
|
265
273
|
|
|
266
274
|
const handleSubmitBets = useCallback(async () => {
|
|
267
275
|
if (!poolId || !config) return;
|
|
@@ -290,6 +298,9 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
290
298
|
|
|
291
299
|
await placeTDBets(poolId, betsToSubmit);
|
|
292
300
|
|
|
301
|
+
// Snapshot submitted state for change detection
|
|
302
|
+
setSubmittedBets({ ...bets });
|
|
303
|
+
|
|
293
304
|
// Refresh data
|
|
294
305
|
await Promise.all([refetchEntry(), refetchLB(), refetchPool()]);
|
|
295
306
|
} catch (err: unknown) {
|
|
@@ -353,6 +364,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
353
364
|
betSummary={betSummary}
|
|
354
365
|
leaderboardRows={leaderboardRows}
|
|
355
366
|
onSubmit={poolId ? handleSubmitBets : undefined}
|
|
367
|
+
submittedBets={poolId ? submittedBets : null}
|
|
356
368
|
onViewLeaderboard={poolId ? () => setShowFullLeaderboard(true) : undefined}
|
|
357
369
|
submitting={submitting}
|
|
358
370
|
/>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
5
5
|
import Image from "next/image";
|
|
6
6
|
import { motion, AnimatePresence, useSpring, useTransform, useMotionValue } from "framer-motion";
|
|
7
|
-
import { ChevronDown, Info, X, Play,
|
|
7
|
+
import { ChevronDown, Info, X, Play, Check, Loader2 } from "lucide-react";
|
|
8
8
|
import { IBetSummary, ILeaderboardEntry, IChallengeConfig, IUserBets, deriveParlayGroups, deriveMarketToParlay, calcDisplayReward, optionReward, calcParlayMultiplier } from "./config";
|
|
9
9
|
import { OUTFIT, MARKET_ICONS, PointsIcon, SelectedCheck, AiInsightButton } from "./constants";
|
|
10
10
|
import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
@@ -37,8 +37,10 @@ interface PreMatchGameProps {
|
|
|
37
37
|
leaderboardRows: ILeaderboardEntry[];
|
|
38
38
|
/** Render only the markets section (no header/balance/leaderboard) — used for card preview */
|
|
39
39
|
marketsOnly?: boolean;
|
|
40
|
-
/** Called when user taps
|
|
40
|
+
/** Called when user taps the action button to submit/re-submit bets. */
|
|
41
41
|
onSubmit?: () => Promise<void> | void;
|
|
42
|
+
/** Snapshot of bets as last saved to the server — drives saved/changed button states. */
|
|
43
|
+
submittedBets?: IUserBets | null;
|
|
42
44
|
/** Called when user taps "See Full Leaderboard" */
|
|
43
45
|
onViewLeaderboard?: () => void;
|
|
44
46
|
/** v1: parlays are cosmetic, shows "Coming in v2". Default = false (hides section) */
|
|
@@ -48,9 +50,23 @@ interface PreMatchGameProps {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOptionClick, onAmountSelect, betSummary, leaderboardRows, marketsOnly, onSubmit, onViewLeaderboard, parlayEnabled = false, submitting = false }: PreMatchGameProps) => {
|
|
53
|
+
export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOptionClick, onAmountSelect, betSummary, leaderboardRows, marketsOnly, onSubmit, submittedBets, onViewLeaderboard, parlayEnabled = false, submitting = false }: PreMatchGameProps) => {
|
|
52
54
|
const { selectedCount, compoundMultiplier, totalEntry, compoundedReward, remainingBalance, riskPercent } = betSummary;
|
|
53
55
|
|
|
56
|
+
// Button state: fresh (no prior submit), saved (submitted, no changes), changed (submitted, user modified)
|
|
57
|
+
const buttonState = useMemo<"fresh" | "saved" | "changed">(() => {
|
|
58
|
+
if (!submittedBets) return "fresh";
|
|
59
|
+
const subKeys = Object.keys(submittedBets);
|
|
60
|
+
const curKeys = Object.keys(bets);
|
|
61
|
+
if (subKeys.length !== curKeys.length) return "changed";
|
|
62
|
+
for (const k of curKeys) {
|
|
63
|
+
const cur = bets[Number(k)];
|
|
64
|
+
const sub = submittedBets[Number(k)];
|
|
65
|
+
if (!sub || cur.optionIdx !== sub.optionIdx || cur.amount !== sub.amount) return "changed";
|
|
66
|
+
}
|
|
67
|
+
return "saved";
|
|
68
|
+
}, [bets, submittedBets]);
|
|
69
|
+
|
|
54
70
|
const goTo = useGamePopupStore(s => s.goTo);
|
|
55
71
|
|
|
56
72
|
// Onboarding — visible whenever no points have been bet yet
|
|
@@ -583,49 +599,48 @@ export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOpt
|
|
|
583
599
|
<AnimatePresence>
|
|
584
600
|
{totalEntry > 0 && (() => {
|
|
585
601
|
const spentPercent = totalEntry / config.startingBalance;
|
|
586
|
-
const fillPercent = 5 + spentPercent * 95;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
className="relative rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center gap-1.5 px-3"
|
|
594
|
-
style={{ background: submitting ? "rgba(34,227,232,0.3)" : "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)", minWidth: 90, height: "100%" }}
|
|
595
|
-
initial={{ opacity: 0, width: 0 }}
|
|
596
|
-
animate={{ opacity: 1, width: "auto" }}
|
|
597
|
-
exit={{ opacity: 0, width: 0 }}
|
|
598
|
-
transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}
|
|
599
|
-
>
|
|
600
|
-
{submitting ? (
|
|
601
|
-
<span className="text-[10px] font-bold text-white/70" style={OUTFIT}>Submitting…</span>
|
|
602
|
-
) : (
|
|
603
|
-
<>
|
|
604
|
-
<Send size={14} className="text-white" />
|
|
605
|
-
<span className="text-[11px] font-bold text-white" style={OUTFIT}>Submit Bets</span>
|
|
606
|
-
</>
|
|
607
|
-
)}
|
|
608
|
-
</motion.button>
|
|
609
|
-
);
|
|
610
|
-
}
|
|
602
|
+
const fillPercent = buttonState === "saved" ? 100 : 5 + spentPercent * 95;
|
|
603
|
+
const fillColor = buttonState === "saved"
|
|
604
|
+
? "linear-gradient(135deg, #22c55e, #16a34a)"
|
|
605
|
+
: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)";
|
|
606
|
+
const bgColor = buttonState === "saved"
|
|
607
|
+
? "linear-gradient(135deg, rgba(34,197,94,0.2), rgba(22,163,74,0.2))"
|
|
608
|
+
: "linear-gradient(135deg, rgba(34,227,232,0.2), rgba(153,69,255,0.2), rgba(248,60,197,0.2))";
|
|
611
609
|
|
|
612
610
|
return (
|
|
613
|
-
<motion.
|
|
611
|
+
<motion.button
|
|
612
|
+
onClick={onSubmit && buttonState !== "saved" ? onSubmit : undefined}
|
|
613
|
+
disabled={submitting || buttonState === "saved"}
|
|
614
614
|
className="relative w-[65px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center"
|
|
615
|
-
style={{ background:
|
|
615
|
+
style={{ background: bgColor, cursor: buttonState === "saved" ? "default" : "pointer" }}
|
|
616
616
|
initial={{ opacity: 0, width: 0 }}
|
|
617
617
|
animate={{ opacity: 1, width: 65 }}
|
|
618
618
|
exit={{ opacity: 0, width: 0 }}
|
|
619
619
|
transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}
|
|
620
620
|
>
|
|
621
|
+
{/* Fill bar */}
|
|
621
622
|
<motion.div
|
|
622
623
|
className="absolute inset-y-0 left-0"
|
|
623
624
|
animate={{ width: `${fillPercent}%` }}
|
|
624
625
|
transition={{ type: "spring", stiffness: 120, damping: 20 }}
|
|
625
|
-
style={{ background:
|
|
626
|
+
style={{ background: fillColor }}
|
|
626
627
|
/>
|
|
627
|
-
|
|
628
|
-
|
|
628
|
+
{/* Pulsing ring when there are unsaved changes */}
|
|
629
|
+
{buttonState === "changed" && (
|
|
630
|
+
<motion.div
|
|
631
|
+
className="absolute inset-0 rounded-xl"
|
|
632
|
+
animate={{ boxShadow: ["0 0 0px rgba(34,227,232,0)", "0 0 10px rgba(34,227,232,0.7)", "0 0 0px rgba(34,227,232,0)"] }}
|
|
633
|
+
transition={{ duration: 1.4, repeat: Infinity, ease: "easeInOut" }}
|
|
634
|
+
/>
|
|
635
|
+
)}
|
|
636
|
+
{/* Icon */}
|
|
637
|
+
{submitting
|
|
638
|
+
? <Loader2 size={22} strokeWidth={2.5} className="relative z-10 animate-spin text-white/70" />
|
|
639
|
+
: buttonState === "saved"
|
|
640
|
+
? <Check size={24} strokeWidth={2.5} className="relative z-10 text-white" />
|
|
641
|
+
: <Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
|
|
642
|
+
}
|
|
643
|
+
</motion.button>
|
|
629
644
|
);
|
|
630
645
|
})()}
|
|
631
646
|
</AnimatePresence>
|