@devrongx/games 0.4.11 → 0.4.13
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
|
@@ -16,7 +16,6 @@ import { GamePopupShell } from "../../core/GamePopupShell";
|
|
|
16
16
|
import { PreMatchIntro } from "./PreMatchIntro";
|
|
17
17
|
import { PreMatchQuestions } from "./PreMatchQuestions";
|
|
18
18
|
import { PreMatchGame } from "./PreMatchGame";
|
|
19
|
-
import { PreMatchSubmitted } from "./PreMatchSubmitted";
|
|
20
19
|
import { PreMatchLive } from "./PreMatchLive";
|
|
21
20
|
import { PreMatchResults } from "./PreMatchResults";
|
|
22
21
|
import { FullLeaderboard } from "./FullLeaderboard";
|
|
@@ -34,7 +33,7 @@ const GAME_ID = "pre-match";
|
|
|
34
33
|
// ─── View logic ────────────────────────────────────────────────────────────────
|
|
35
34
|
|
|
36
35
|
type UserFlowState = "intro" | "questions" | "game";
|
|
37
|
-
type ActiveView = UserFlowState | "
|
|
36
|
+
type ActiveView = UserFlowState | "live" | "results";
|
|
38
37
|
|
|
39
38
|
function getActiveView(
|
|
40
39
|
poolStatus: number | undefined,
|
|
@@ -44,8 +43,7 @@ function getActiveView(
|
|
|
44
43
|
if (poolStatus === undefined) return userFlowState;
|
|
45
44
|
if (poolStatus === TDPoolStatus.COMPLETE || poolStatus === TDPoolStatus.CANCELLED) return "results";
|
|
46
45
|
if (poolStatus === TDPoolStatus.CLOSED || poolStatus === TDPoolStatus.RESOLVING) return hasEntry ? "live" : "results";
|
|
47
|
-
// Pool is open
|
|
48
|
-
if (hasEntry) return "submitted";
|
|
46
|
+
// Pool is open — user can keep editing regardless
|
|
49
47
|
return userFlowState;
|
|
50
48
|
}
|
|
51
49
|
|
|
@@ -122,6 +120,33 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
122
120
|
const [showFullLeaderboard, setShowFullLeaderboard] = useState(false);
|
|
123
121
|
const [submitting, setSubmitting] = useState(false);
|
|
124
122
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
123
|
+
const [submittedBets, setSubmittedBets] = useState<IUserBets | null>(null);
|
|
124
|
+
|
|
125
|
+
// ── Real bets restore: when user has submitted bets, load them into game state ─
|
|
126
|
+
const realBetsRestoredRef = useRef(false);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!poolId || !config || !entryData || realBetsRestoredRef.current) return;
|
|
129
|
+
if (entryData.bets.length === 0) return;
|
|
130
|
+
const loaded: IUserBets = {};
|
|
131
|
+
const pickers: Record<number, number> = {};
|
|
132
|
+
for (const bet of entryData.bets) {
|
|
133
|
+
if (bet.parlay_id !== null) continue;
|
|
134
|
+
const mIdx = config.markets.findIndex((m) => m.backendChallengeId === bet.challenge_id);
|
|
135
|
+
if (mIdx < 0) continue;
|
|
136
|
+
const oIdx = config.markets[mIdx].options.findIndex(
|
|
137
|
+
(o) => o.label.toLowerCase().replace(/\s+/g, "_") === bet.selected_option,
|
|
138
|
+
);
|
|
139
|
+
if (oIdx < 0) continue;
|
|
140
|
+
loaded[mIdx] = { optionIdx: oIdx, amount: bet.coin_amount, parlaySlot: null };
|
|
141
|
+
pickers[mIdx] = oIdx;
|
|
142
|
+
}
|
|
143
|
+
if (Object.keys(loaded).length === 0) return;
|
|
144
|
+
setBets(loaded);
|
|
145
|
+
setSubmittedBets(loaded);
|
|
146
|
+
setExpandedPicker(pickers);
|
|
147
|
+
setUserFlowState("game");
|
|
148
|
+
realBetsRestoredRef.current = true;
|
|
149
|
+
}, [poolId, config, entryData]);
|
|
125
150
|
|
|
126
151
|
// ── Draft restore: when entry + config load, seed bets from draft_selections ─
|
|
127
152
|
const draftRestoredRef = useRef(false);
|
|
@@ -267,6 +292,9 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
267
292
|
|
|
268
293
|
await placeTDBets(poolId, betsToSubmit);
|
|
269
294
|
|
|
295
|
+
// Snapshot submitted state for change detection
|
|
296
|
+
setSubmittedBets({ ...bets });
|
|
297
|
+
|
|
270
298
|
// Refresh data
|
|
271
299
|
await Promise.all([refetchEntry(), refetchLB(), refetchPool()]);
|
|
272
300
|
} catch (err: unknown) {
|
|
@@ -276,24 +304,6 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
276
304
|
}
|
|
277
305
|
}, [poolId, config, bets, hasEntry, refetchEntry, refetchLB, refetchPool]);
|
|
278
306
|
|
|
279
|
-
const handleAdjust = useCallback(async () => {
|
|
280
|
-
// Load server bets back into local state
|
|
281
|
-
if (entryData?.bets && config) {
|
|
282
|
-
const loaded: IUserBets = {};
|
|
283
|
-
for (const bet of entryData.bets) {
|
|
284
|
-
if (bet.parlay_id !== null) continue; // skip parlays for now
|
|
285
|
-
const mIdx = config.markets.findIndex((m) => m.backendChallengeId === bet.challenge_id);
|
|
286
|
-
if (mIdx >= 0) {
|
|
287
|
-
// Find option by matching the stored key to a label approximation
|
|
288
|
-
// (we store selected_option as the label key from the pool option)
|
|
289
|
-
const oIdx = config.markets[mIdx].options.findIndex((_, i) => i === 0); // fallback: first option
|
|
290
|
-
loaded[mIdx] = { optionIdx: oIdx, amount: bet.coin_amount, parlaySlot: null };
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
setBets(loaded);
|
|
294
|
-
}
|
|
295
|
-
setUserFlowState("game");
|
|
296
|
-
}, [entryData, config]);
|
|
297
307
|
|
|
298
308
|
// ── Loading ────────────────────────────────────────────────────────────────
|
|
299
309
|
if (poolId && (poolLoading || (!config && !poolLoading))) {
|
|
@@ -348,22 +358,12 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
348
358
|
betSummary={betSummary}
|
|
349
359
|
leaderboardRows={leaderboardRows}
|
|
350
360
|
onSubmit={poolId ? handleSubmitBets : undefined}
|
|
361
|
+
submittedBets={poolId ? submittedBets : null}
|
|
351
362
|
onViewLeaderboard={poolId ? () => setShowFullLeaderboard(true) : undefined}
|
|
352
363
|
submitting={submitting}
|
|
353
364
|
/>
|
|
354
365
|
)}
|
|
355
366
|
|
|
356
|
-
{activeView === "submitted" && (
|
|
357
|
-
<PreMatchSubmitted
|
|
358
|
-
config={config}
|
|
359
|
-
poolId={poolId!}
|
|
360
|
-
entryData={entryData}
|
|
361
|
-
rankings={rankings}
|
|
362
|
-
onAdjust={handleAdjust}
|
|
363
|
-
onViewLeaderboard={() => setShowFullLeaderboard(true)}
|
|
364
|
-
/>
|
|
365
|
-
)}
|
|
366
|
-
|
|
367
367
|
{activeView === "live" && (
|
|
368
368
|
<PreMatchLive
|
|
369
369
|
config={config}
|
|
@@ -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>
|