@devrongx/games 0.4.5 → 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,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) => {
|
|
@@ -268,7 +324,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
268
324
|
)}
|
|
269
325
|
|
|
270
326
|
{activeView === "intro" && (
|
|
271
|
-
<PreMatchIntro config={config} />
|
|
327
|
+
<PreMatchIntro config={config} onComplete={() => setUserFlowState("questions")} />
|
|
272
328
|
)}
|
|
273
329
|
|
|
274
330
|
{activeView === "questions" && (
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
// @devrongx/games — games/prematch-bets/PreMatchIntro.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
5
5
|
import Image from "next/image";
|
|
6
6
|
import { motion, AnimatePresence } from "framer-motion";
|
|
7
|
-
import { Coins, Trophy,
|
|
8
|
-
import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
7
|
+
import { Coins, Trophy, Pause, Rewind, FastForward, RotateCcw } from "lucide-react";
|
|
9
8
|
import { IChallengeConfig } from "./config";
|
|
10
9
|
|
|
11
10
|
const OUTFIT = { fontFamily: "Outfit, sans-serif" };
|
|
12
|
-
const TOTAL_SLIDES = 6;
|
|
13
|
-
const SLIDE_DURATIONS = [5500, 5000, 5500, 7000, 5500, 5500];
|
|
14
11
|
|
|
15
12
|
// Countdown hook for match start
|
|
16
13
|
const useCountdown = (targetIso: string) => {
|
|
@@ -34,8 +31,8 @@ const useCountdown = (targetIso: string) => {
|
|
|
34
31
|
return text;
|
|
35
32
|
};
|
|
36
33
|
|
|
37
|
-
// Slide props — all slides receive config
|
|
38
|
-
interface SlideProps { config: IChallengeConfig }
|
|
34
|
+
// Slide props — all slides receive config + onComplete
|
|
35
|
+
interface SlideProps { config: IChallengeConfig; onComplete: () => void }
|
|
39
36
|
|
|
40
37
|
// ── Slide 0: Logo reveal + match info + countdown ──
|
|
41
38
|
const LogoSlide = ({ config }: SlideProps) => {
|
|
@@ -61,7 +58,7 @@ const LogoSlide = ({ config }: SlideProps) => {
|
|
|
61
58
|
>
|
|
62
59
|
<svg width="44" height="44" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
63
60
|
<g clipPath="url(#clip_intro_logo)">
|
|
64
|
-
<path d="M14.0024 12.5249C19.1884 12.5249 22.4314 13.6319 23.4234 14.4939C20.9354 15.1919 17.6354 15.6219 14.0024 15.6219C10.3694 15.6219 7.06937 15.1919 4.58137 14.4939C5.57337 13.6319 8.81637 12.5249 14.0024 12.5249ZM20.0574 3.43286C20.1079 3.17257 20.2598 2.94303 20.4796 2.79469C20.6994 2.64636 20.9691 2.59139 21.2294 2.64186L25.1564 3.40586C25.4125 3.46068 25.6368 3.61378 25.7813 3.83225C25.9257 4.05073 25.9786 4.31714 25.9287 4.57424C25.8788 4.83134 25.7301 5.0586 25.5144 5.20719C25.2987 5.35577 25.0334 5.41382 24.7754 5.36886L24.0024 5.21886V6.75386C23.3684 6.56786 22.7024 6.40086 22.0024 6.25686V4.82986L20.8484 4.60586C20.588 4.55506 20.3584 4.40298 20.2101 4.18304C20.0618 3.9631 20.0069 3.69328 20.0574 3.43286ZM11.0024 3.00586C11.0024 2.74064 11.1077 2.48629 11.2953 2.29875C11.4828 2.11122 11.7371 2.00586 12.0024 2.00586H16.0024C16.2676 2.00586 16.5219 2.11122 16.7095 2.29875C16.897 2.48629 17.0024 2.74064 17.0024 3.00586C17.0024 3.27108 16.897 3.52543 16.7095 3.71297C16.5219 3.9005 16.2676 4.00586 16.0024 4.00586H15.0024V5.52186C14.3358 5.50186 13.6689 5.50186 13.0024 5.52186V4.00586H12.0024C11.7371 4.00586 11.4828 3.9005 11.2953 3.71297C11.1077 3.52543 11.0024 3.27108 11.0024 3.00586ZM2.05737 4.57886C2.00687 4.31843 2.06182 4.04862 2.21014 3.82868C2.35845 3.60874 2.588 3.45666 2.84837 3.40586L6.77537 2.64186C6.90432 2.61684 7.03695 2.61747 7.16566 2.64371C7.29438 2.66995 7.41666 2.72128 7.52554 2.79478C7.63442 2.86827 7.72775 2.9625 7.80021 3.07207C7.
|
|
61
|
+
<path d="M14.0024 12.5249C19.1884 12.5249 22.4314 13.6319 23.4234 14.4939C20.9354 15.1919 17.6354 15.6219 14.0024 15.6219C10.3694 15.6219 7.06937 15.1919 4.58137 14.4939C5.57337 13.6319 8.81637 12.5249 14.0024 12.5249ZM20.0574 3.43286C20.1079 3.17257 20.2598 2.94303 20.4796 2.79469C20.6994 2.64636 20.9691 2.59139 21.2294 2.64186L25.1564 3.40586C25.4125 3.46068 25.6368 3.61378 25.7813 3.83225C25.9257 4.05073 25.9786 4.31714 25.9287 4.57424C25.8788 4.83134 25.7301 5.0586 25.5144 5.20719C25.2987 5.35577 25.0334 5.41382 24.7754 5.36886L24.0024 5.21886V6.75386C23.3684 6.56786 22.7024 6.40086 22.0024 6.25686V4.82986L20.8484 4.60586C20.588 4.55506 20.3584 4.40298 20.2101 4.18304C20.0618 3.9631 20.0069 3.69328 20.0574 3.43286ZM11.0024 3.00586C11.0024 2.74064 11.1077 2.48629 11.2953 2.29875C11.4828 2.11122 11.7371 2.00586 12.0024 2.00586H16.0024C16.2676 2.00586 16.5219 2.11122 16.7095 2.29875C16.897 2.48629 17.0024 2.74064 17.0024 3.00586C17.0024 3.27108 16.897 3.52543 16.7095 3.71297C16.5219 3.9005 16.2676 4.00586 16.0024 4.00586H15.0024V5.52186C14.3358 5.50186 13.6689 5.50186 13.0024 5.52186V4.00586H12.0024C11.7371 4.00586 11.4828 3.9005 11.2953 3.71297C11.1077 3.52543 11.0024 3.27108 11.0024 3.00586ZM2.05737 4.57886C2.00687 4.31843 2.06182 4.04862 2.21014 3.82868C2.35845 3.60874 2.588 3.45666 2.84837 3.40586L6.77537 2.64186C6.90432 2.61684 7.03695 2.61747 7.16566 2.64371C7.29438 2.66995 7.41666 2.72128 7.52554 2.79478C7.63442 2.86827 7.72775 2.9625 7.80021 3.07207C7.92285 3.18163 7.92285 3.3044 7.94787 3.43336C7.97288 3.56232 7.97225 3.69494 7.94602 3.82366C7.91978 3.95237 7.86845 4.07466 7.79495 4.18353C7.72145 4.29241 7.62723 4.38575 7.51766 4.45821C7.40809 4.53067 7.28532 4.58084 7.15637 4.60586L6.00237 4.82986V6.25686C5.30237 6.39986 4.63637 6.56786 4.00237 6.75386V5.21886L3.23037 5.36886C3.10147 5.39401 2.96889 5.39352 2.84018 5.36743C2.71148 5.34134 2.58917 5.29016 2.48025 5.2168C2.37133 5.14344 2.27792 5.04935 2.20536 4.93989C2.1328 4.83043 2.08251 4.70775 2.05737 4.57886Z" fill="url(#paint0_intro_logo)" />
|
|
65
62
|
<path d="M28 11.3139C28 12.3309 26.849 13.2629 24.934 13.9999C23.684 12.0439 18.821 11.0249 14 11.0249C9.179 11.0249 4.316 12.0439 3.066 13.9999C1.151 13.2629 0 12.3309 0 11.3139C0 8.93486 6.268 7.00586 14 7.00586C21.732 7.00586 28 8.93486 28 11.3139Z" fill="url(#paint1_intro_logo)" />
|
|
66
63
|
<path d="M14 17.122C8.484 17.122 2.667 16.126 0 13.998V21.699C0 23.761 4.712 25.482 11.001 25.905V21.006C11.001 20.7408 11.1064 20.4865 11.2939 20.2989C11.4814 20.1114 11.7358 20.006 12.001 20.006H16.002C16.2672 20.006 16.5216 20.1114 16.7091 20.2989C16.8966 20.4865 17.002 20.7408 17.002 21.006V25.904C23.29 25.481 28 23.76 28 21.699V13.998C25.333 16.126 19.516 17.122 14 17.122Z" fill="url(#paint2_intro_logo)" />
|
|
67
64
|
</g>
|
|
@@ -119,30 +116,11 @@ const LogoSlide = ({ config }: SlideProps) => {
|
|
|
119
116
|
{countdown}
|
|
120
117
|
</span>
|
|
121
118
|
</motion.div>
|
|
122
|
-
{/* Title — appears last, with cyan fill sweep */}
|
|
123
|
-
<motion.p
|
|
124
|
-
initial={{ opacity: 0, y: 20 }}
|
|
125
|
-
animate={{ opacity: 1, y: 0 }}
|
|
126
|
-
transition={{ duration: 0.5, delay: 2.8 }}
|
|
127
|
-
className="text-[18px] font-bold tracking-wider uppercase mt-4"
|
|
128
|
-
style={{
|
|
129
|
-
...OUTFIT,
|
|
130
|
-
background: "linear-gradient(90deg, #22E3E8 0%, #22E3E8 50%, rgba(255,255,255,0.35) 50.1%, rgba(255,255,255,0.35) 100%)",
|
|
131
|
-
backgroundSize: "200% 100%",
|
|
132
|
-
backgroundPosition: "100% 0",
|
|
133
|
-
WebkitBackgroundClip: "text",
|
|
134
|
-
WebkitTextFillColor: "transparent",
|
|
135
|
-
backgroundClip: "text",
|
|
136
|
-
animation: "textFillSweep 2s ease-out 3.3s forwards",
|
|
137
|
-
}}
|
|
138
|
-
>
|
|
139
|
-
{config.title}
|
|
140
|
-
</motion.p>
|
|
141
119
|
</motion.div>
|
|
142
120
|
);
|
|
143
121
|
};
|
|
144
122
|
|
|
145
|
-
// ── Slide 1: Buy in with a match pass ──
|
|
123
|
+
// ── Slide 1: Buy in with a match pass (only shown when entryFee > 0) ──
|
|
146
124
|
const EntrySlide = ({ config }: SlideProps) => (
|
|
147
125
|
<motion.div
|
|
148
126
|
className="flex flex-col items-center gap-4"
|
|
@@ -150,7 +128,6 @@ const EntrySlide = ({ config }: SlideProps) => (
|
|
|
150
128
|
animate="animate"
|
|
151
129
|
exit="exit"
|
|
152
130
|
>
|
|
153
|
-
{/* Headline — appears first */}
|
|
154
131
|
<motion.p
|
|
155
132
|
initial={{ opacity: 0, y: 20 }}
|
|
156
133
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -160,7 +137,6 @@ const EntrySlide = ({ config }: SlideProps) => (
|
|
|
160
137
|
>
|
|
161
138
|
Buy in with a match pass
|
|
162
139
|
</motion.p>
|
|
163
|
-
{/* Price + USDC — appears after reading the headline */}
|
|
164
140
|
<motion.div
|
|
165
141
|
initial={{ opacity: 0, y: 20 }}
|
|
166
142
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -170,7 +146,6 @@ const EntrySlide = ({ config }: SlideProps) => (
|
|
|
170
146
|
<span className="text-[52px] font-bold text-white leading-none" style={OUTFIT}>${config.entryFee}</span>
|
|
171
147
|
<Image src="/icons/ic_usdc_hd.png" alt="USDC" width={52} height={52} className="rounded-full" />
|
|
172
148
|
</motion.div>
|
|
173
|
-
{/* Subtitle — appears last */}
|
|
174
149
|
<motion.p
|
|
175
150
|
initial={{ opacity: 0, y: 16 }}
|
|
176
151
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -183,7 +158,7 @@ const EntrySlide = ({ config }: SlideProps) => (
|
|
|
183
158
|
</motion.div>
|
|
184
159
|
);
|
|
185
160
|
|
|
186
|
-
// ── Slide 2: Get
|
|
161
|
+
// ── Slide 2: Get prediction points ──
|
|
187
162
|
const PointsSlide = ({ config }: SlideProps) => (
|
|
188
163
|
<motion.div
|
|
189
164
|
className="flex flex-col items-center"
|
|
@@ -191,7 +166,6 @@ const PointsSlide = ({ config }: SlideProps) => (
|
|
|
191
166
|
animate="animate"
|
|
192
167
|
exit="exit"
|
|
193
168
|
>
|
|
194
|
-
{/* "You get" — appears first */}
|
|
195
169
|
<motion.p
|
|
196
170
|
initial={{ opacity: 0, y: 20 }}
|
|
197
171
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -201,7 +175,6 @@ const PointsSlide = ({ config }: SlideProps) => (
|
|
|
201
175
|
>
|
|
202
176
|
You get
|
|
203
177
|
</motion.p>
|
|
204
|
-
{/* Big number + logo — appears after "You get" is read */}
|
|
205
178
|
<motion.div
|
|
206
179
|
initial={{ opacity: 0, y: 20 }}
|
|
207
180
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -222,7 +195,6 @@ const PointsSlide = ({ config }: SlideProps) => (
|
|
|
222
195
|
{config.startingBalance.toLocaleString()}
|
|
223
196
|
</span>
|
|
224
197
|
</motion.div>
|
|
225
|
-
{/* "prediction points" — appears after the number lands */}
|
|
226
198
|
<motion.p
|
|
227
199
|
initial={{ opacity: 0, y: 16 }}
|
|
228
200
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -232,7 +204,6 @@ const PointsSlide = ({ config }: SlideProps) => (
|
|
|
232
204
|
>
|
|
233
205
|
prediction points to bet with
|
|
234
206
|
</motion.p>
|
|
235
|
-
{/* Delayed tip — appears last */}
|
|
236
207
|
<motion.p
|
|
237
208
|
initial={{ opacity: 0, y: 10 }}
|
|
238
209
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -251,8 +222,8 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
251
222
|
|
|
252
223
|
useEffect(() => {
|
|
253
224
|
const timers = [
|
|
254
|
-
setTimeout(() => setDemoStep(1), 2800),
|
|
255
|
-
setTimeout(() => setDemoStep(2), 4200),
|
|
225
|
+
setTimeout(() => setDemoStep(1), 2800),
|
|
226
|
+
setTimeout(() => setDemoStep(2), 4200),
|
|
256
227
|
];
|
|
257
228
|
return () => timers.forEach(clearTimeout);
|
|
258
229
|
}, []);
|
|
@@ -264,7 +235,6 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
264
235
|
animate="animate"
|
|
265
236
|
exit="exit"
|
|
266
237
|
>
|
|
267
|
-
{/* Headline — appears first */}
|
|
268
238
|
<motion.p
|
|
269
239
|
initial={{ opacity: 0, y: 20 }}
|
|
270
240
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -275,7 +245,6 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
275
245
|
{config.markets.length} markets to predict
|
|
276
246
|
</motion.p>
|
|
277
247
|
|
|
278
|
-
{/* Floating market card — appears after headline is read */}
|
|
279
248
|
<motion.div
|
|
280
249
|
initial={{ opacity: 0, y: 20 }}
|
|
281
250
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -286,28 +255,19 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
286
255
|
border: "1px solid rgba(255,255,255,0.08)",
|
|
287
256
|
}}
|
|
288
257
|
>
|
|
289
|
-
{/* Market question */}
|
|
290
258
|
<div className="flex items-center gap-2 mb-2.5">
|
|
291
259
|
<Coins size={14} className="text-[#22E3E8]/50 flex-shrink-0" />
|
|
292
260
|
<span className="text-[13px] text-white font-semibold" style={OUTFIT}>
|
|
293
261
|
Who wins the toss?
|
|
294
262
|
</span>
|
|
295
263
|
</div>
|
|
296
|
-
{/* Options */}
|
|
297
264
|
<div className="grid grid-cols-2 gap-2">
|
|
298
|
-
{/* CSK option — gets selected */}
|
|
299
265
|
<div
|
|
300
266
|
className="flex items-center justify-between px-2.5 py-2 rounded-sm transition-all duration-500"
|
|
301
267
|
style={{
|
|
302
|
-
background: demoStep >= 1
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
borderLeft: demoStep >= 1
|
|
306
|
-
? "1px solid transparent"
|
|
307
|
-
: "1px solid rgba(255,255,255,0.12)",
|
|
308
|
-
borderBottom: demoStep >= 1
|
|
309
|
-
? "1px solid transparent"
|
|
310
|
-
: "1px solid rgba(255,255,255,0.06)",
|
|
268
|
+
background: demoStep >= 1 ? "linear-gradient(135deg, #22E3E8, #9945FF)" : "transparent",
|
|
269
|
+
borderLeft: demoStep >= 1 ? "1px solid transparent" : "1px solid rgba(255,255,255,0.12)",
|
|
270
|
+
borderBottom: demoStep >= 1 ? "1px solid transparent" : "1px solid rgba(255,255,255,0.06)",
|
|
311
271
|
borderTop: "1px solid transparent",
|
|
312
272
|
borderRight: "1px solid transparent",
|
|
313
273
|
}}
|
|
@@ -330,7 +290,6 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
330
290
|
1.4x
|
|
331
291
|
</span>
|
|
332
292
|
</div>
|
|
333
|
-
{/* RCB option — stays default */}
|
|
334
293
|
<div
|
|
335
294
|
className="flex items-center justify-between px-2.5 py-2 rounded-sm"
|
|
336
295
|
style={{
|
|
@@ -344,7 +303,6 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
344
303
|
<span className="text-[10px] font-bold text-[#22E3E8]" style={OUTFIT}>1.4x</span>
|
|
345
304
|
</div>
|
|
346
305
|
</div>
|
|
347
|
-
{/* Amount picker — appears at step 2 */}
|
|
348
306
|
<AnimatePresence>
|
|
349
307
|
{demoStep >= 2 && (
|
|
350
308
|
<motion.div
|
|
@@ -369,15 +327,6 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
369
327
|
border: "1px solid rgba(34,227,232,0.2)",
|
|
370
328
|
}}
|
|
371
329
|
>
|
|
372
|
-
{amt === 100 && (
|
|
373
|
-
<div
|
|
374
|
-
className="absolute inset-0 pointer-events-none animate-shine"
|
|
375
|
-
style={{
|
|
376
|
-
background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%)",
|
|
377
|
-
backgroundSize: "200% 100%",
|
|
378
|
-
}}
|
|
379
|
-
/>
|
|
380
|
-
)}
|
|
381
330
|
<Image src="/iamgame_square_logo.jpg" alt="" width={7} height={7} className="rounded-[1px] relative z-[1]" />
|
|
382
331
|
<span
|
|
383
332
|
className={`text-[9px] font-bold relative z-[1] ${amt === 100 ? "text-black" : "text-[#22E3E8]"}`}
|
|
@@ -392,7 +341,6 @@ const MarketsSlide = ({ config }: SlideProps) => {
|
|
|
392
341
|
</AnimatePresence>
|
|
393
342
|
</motion.div>
|
|
394
343
|
|
|
395
|
-
{/* Subtitle — appears after demo plays */}
|
|
396
344
|
<motion.p
|
|
397
345
|
initial={{ opacity: 0, y: 16 }}
|
|
398
346
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -414,7 +362,6 @@ const MultiplierSlide = ({ config }: SlideProps) => (
|
|
|
414
362
|
animate="animate"
|
|
415
363
|
exit="exit"
|
|
416
364
|
>
|
|
417
|
-
{/* First line — appears immediately */}
|
|
418
365
|
<motion.p
|
|
419
366
|
initial={{ opacity: 0, y: 20 }}
|
|
420
367
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -424,7 +371,6 @@ const MultiplierSlide = ({ config }: SlideProps) => (
|
|
|
424
371
|
>
|
|
425
372
|
Stack predictions.
|
|
426
373
|
</motion.p>
|
|
427
|
-
{/* Second line — after first is read */}
|
|
428
374
|
<motion.p
|
|
429
375
|
initial={{ opacity: 0, y: 20 }}
|
|
430
376
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -434,7 +380,6 @@ const MultiplierSlide = ({ config }: SlideProps) => (
|
|
|
434
380
|
>
|
|
435
381
|
Multiply your odds.
|
|
436
382
|
</motion.p>
|
|
437
|
-
{/* Big number — the reveal moment */}
|
|
438
383
|
<motion.div
|
|
439
384
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
440
385
|
animate={{ opacity: 1, scale: 1 }}
|
|
@@ -453,7 +398,6 @@ const MultiplierSlide = ({ config }: SlideProps) => (
|
|
|
453
398
|
{config.compoundMultipliers[config.compoundMultipliers.length - 1]}x
|
|
454
399
|
</span>
|
|
455
400
|
</motion.div>
|
|
456
|
-
{/* Label — after number lands */}
|
|
457
401
|
<motion.p
|
|
458
402
|
initial={{ opacity: 0, y: 16 }}
|
|
459
403
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -466,25 +410,33 @@ const MultiplierSlide = ({ config }: SlideProps) => (
|
|
|
466
410
|
</motion.div>
|
|
467
411
|
);
|
|
468
412
|
|
|
413
|
+
// Free leaderboard payouts: ranks 1-10, $25 down to $2.5 in $2.5 steps
|
|
414
|
+
const FREE_PAYOUTS = Array.from({ length: 10 }, (_, i) => ({
|
|
415
|
+
rank: i + 1,
|
|
416
|
+
usdc: 25 - i * 2.5,
|
|
417
|
+
}));
|
|
418
|
+
|
|
469
419
|
// ── Slide 5: Win USDC ──
|
|
470
|
-
const WinSlide = ({ config
|
|
471
|
-
const
|
|
420
|
+
const WinSlide = ({ config, onComplete }: SlideProps) => {
|
|
421
|
+
const isFree = config.entryFee === 0;
|
|
422
|
+
|
|
472
423
|
return (
|
|
473
424
|
<motion.div
|
|
474
|
-
className="flex flex-col items-center gap-
|
|
425
|
+
className="flex flex-col items-center gap-3 w-full max-w-[280px]"
|
|
475
426
|
initial="initial"
|
|
476
427
|
animate="animate"
|
|
477
428
|
exit="exit"
|
|
478
429
|
>
|
|
479
|
-
{/* Trophy
|
|
430
|
+
{/* Trophy */}
|
|
480
431
|
<motion.div
|
|
481
432
|
initial={{ opacity: 0, y: 20 }}
|
|
482
433
|
animate={{ opacity: 1, y: 0 }}
|
|
483
434
|
transition={{ duration: 0.5, delay: 0.2 }}
|
|
484
435
|
>
|
|
485
|
-
<Trophy size={
|
|
436
|
+
<Trophy size={32} className="text-[#FFD700]" />
|
|
486
437
|
</motion.div>
|
|
487
|
-
|
|
438
|
+
|
|
439
|
+
{/* Headline */}
|
|
488
440
|
<motion.p
|
|
489
441
|
initial={{ opacity: 0, y: 20 }}
|
|
490
442
|
animate={{ opacity: 1, y: 0 }}
|
|
@@ -494,34 +446,80 @@ const WinSlide = ({ config: _config }: SlideProps) => {
|
|
|
494
446
|
>
|
|
495
447
|
Top the leaderboard.
|
|
496
448
|
</motion.p>
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
449
|
+
|
|
450
|
+
{isFree ? (
|
|
451
|
+
/* Free game: rank/reward table */
|
|
452
|
+
<motion.div
|
|
453
|
+
initial={{ opacity: 0, y: 20 }}
|
|
454
|
+
animate={{ opacity: 1, y: 0 }}
|
|
455
|
+
transition={{ duration: 0.6, delay: 1.4 }}
|
|
456
|
+
className="w-full rounded-xl overflow-hidden"
|
|
457
|
+
style={{ border: "1px solid rgba(255,255,255,0.08)", background: "rgba(255,255,255,0.02)" }}
|
|
458
|
+
>
|
|
459
|
+
{/* Table header */}
|
|
460
|
+
<div className="flex items-center justify-between px-3 py-1.5" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)", background: "rgba(255,255,255,0.03)" }}>
|
|
461
|
+
<span className="text-[9px] font-bold text-white/50 uppercase tracking-widest" style={OUTFIT}>Rank</span>
|
|
462
|
+
<div className="flex items-center gap-1">
|
|
463
|
+
<Image src="/icons/ic_usdc_hd.png" alt="USDC" width={11} height={11} className="rounded-full" />
|
|
464
|
+
<span className="text-[9px] font-bold text-white/50 uppercase tracking-widest" style={OUTFIT}>Reward</span>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
{/* Rows */}
|
|
468
|
+
{FREE_PAYOUTS.map((row, idx) => (
|
|
469
|
+
<motion.div
|
|
470
|
+
key={row.rank}
|
|
471
|
+
initial={{ opacity: 0, x: -8 }}
|
|
472
|
+
animate={{ opacity: 1, x: 0 }}
|
|
473
|
+
transition={{ delay: 1.6 + idx * 0.06, duration: 0.2 }}
|
|
474
|
+
className="flex items-center justify-between px-3 py-1.5"
|
|
475
|
+
style={{
|
|
476
|
+
borderBottom: idx < FREE_PAYOUTS.length - 1 ? "1px solid rgba(255,255,255,0.04)" : "none",
|
|
477
|
+
background: idx === 0 ? "rgba(255,215,0,0.04)" : idx < 3 ? "rgba(255,255,255,0.015)" : "transparent",
|
|
478
|
+
}}
|
|
479
|
+
>
|
|
480
|
+
<div className="flex items-center gap-2">
|
|
481
|
+
{idx === 0 && <span style={{ fontSize: 11 }}>🥇</span>}
|
|
482
|
+
{idx === 1 && <span style={{ fontSize: 11 }}>🥈</span>}
|
|
483
|
+
{idx === 2 && <span style={{ fontSize: 11 }}>🥉</span>}
|
|
484
|
+
{idx >= 3 && (
|
|
485
|
+
<span className="text-[10px] text-white/40 font-semibold w-[14px] text-center" style={OUTFIT}>#{row.rank}</span>
|
|
486
|
+
)}
|
|
487
|
+
{idx < 3 && (
|
|
488
|
+
<span className="text-[10px] font-semibold" style={{ ...OUTFIT, color: idx === 0 ? "#FFD700" : idx === 1 ? "#C0C0C0" : "#CD7F32" }}>
|
|
489
|
+
#{row.rank}
|
|
490
|
+
</span>
|
|
491
|
+
)}
|
|
492
|
+
</div>
|
|
493
|
+
<span
|
|
494
|
+
className="text-[11px] font-bold"
|
|
495
|
+
style={{ ...OUTFIT, color: idx === 0 ? "#FFD700" : idx < 3 ? "rgba(255,255,255,0.85)" : "rgba(255,255,255,0.6)" }}
|
|
496
|
+
>
|
|
497
|
+
${row.usdc.toFixed(2)}
|
|
498
|
+
</span>
|
|
499
|
+
</motion.div>
|
|
500
|
+
))}
|
|
501
|
+
</motion.div>
|
|
502
|
+
) : (
|
|
503
|
+
/* Paid game: "Win real USDC" */
|
|
504
|
+
<motion.div
|
|
505
|
+
initial={{ opacity: 0, y: 20 }}
|
|
506
|
+
animate={{ opacity: 1, y: 0 }}
|
|
507
|
+
transition={{ duration: 0.6, delay: 1.8 }}
|
|
508
|
+
className="flex items-center gap-2"
|
|
509
|
+
>
|
|
510
|
+
<span className="text-[18px] text-white/70 font-medium" style={OUTFIT}>Win real</span>
|
|
511
|
+
<Image src="/icons/ic_usdc_hd.png" alt="USDC" width={22} height={22} className="rounded-full" />
|
|
512
|
+
<span className="text-[18px] text-[#2775CA] font-bold" style={OUTFIT}>USDC</span>
|
|
513
|
+
</motion.div>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
{/* CTA button */}
|
|
519
517
|
<motion.button
|
|
520
518
|
initial={{ opacity: 0, y: 16 }}
|
|
521
519
|
animate={{ opacity: 1, y: 0 }}
|
|
522
|
-
transition={{ duration: 0.5, delay: 3.5 }}
|
|
523
|
-
onClick={
|
|
524
|
-
className="mt-
|
|
520
|
+
transition={{ duration: 0.5, delay: isFree ? 3.4 : 3.5 }}
|
|
521
|
+
onClick={onComplete}
|
|
522
|
+
className="mt-2 px-6 py-2.5 rounded-full cursor-pointer"
|
|
525
523
|
style={{ background: "linear-gradient(135deg, #22E3E8, #9945FF)" }}
|
|
526
524
|
>
|
|
527
525
|
<span className="text-[14px] font-bold text-black" style={OUTFIT}>Build Your Bets</span>
|
|
@@ -531,30 +529,43 @@ const WinSlide = ({ config: _config }: SlideProps) => {
|
|
|
531
529
|
};
|
|
532
530
|
|
|
533
531
|
|
|
534
|
-
export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
535
|
-
const goTo = useGamePopupStore(s => s.goTo);
|
|
532
|
+
export const PreMatchIntro = ({ config, onComplete }: { config: IChallengeConfig; onComplete: () => void }) => {
|
|
536
533
|
const [slideIdx, setSlideIdx] = useState(0);
|
|
537
534
|
const [direction, setDirection] = useState(1);
|
|
538
535
|
const [paused, setPaused] = useState(false);
|
|
539
536
|
const [slideFill, setSlideFill] = useState(0);
|
|
540
537
|
const [finished, setFinished] = useState(false);
|
|
541
538
|
|
|
539
|
+
// Dynamic slide list — skip EntrySlide when entry fee is 0
|
|
540
|
+
const slides = useMemo(() => {
|
|
541
|
+
type SlideEntry = { component: React.ComponentType<SlideProps>; duration: number };
|
|
542
|
+
const all: SlideEntry[] = [
|
|
543
|
+
{ component: LogoSlide, duration: 5500 },
|
|
544
|
+
{ component: EntrySlide, duration: 5000 },
|
|
545
|
+
{ component: PointsSlide, duration: 5500 },
|
|
546
|
+
{ component: MarketsSlide, duration: 7000 },
|
|
547
|
+
{ component: MultiplierSlide, duration: 5500 },
|
|
548
|
+
{ component: WinSlide, duration: 5500 },
|
|
549
|
+
];
|
|
550
|
+
return config.entryFee === 0 ? all.filter((_, i) => i !== 1) : all;
|
|
551
|
+
}, [config.entryFee]);
|
|
552
|
+
|
|
553
|
+
const totalSlides = slides.length;
|
|
554
|
+
|
|
542
555
|
// Refs for timing
|
|
543
556
|
const slideStartRef = useRef(Date.now());
|
|
544
|
-
const pausedElapsedRef = useRef(0);
|
|
557
|
+
const pausedElapsedRef = useRef(0);
|
|
545
558
|
const rafRef = useRef<number>(0);
|
|
546
559
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
547
560
|
|
|
548
|
-
// Clear auto-advance timer
|
|
549
561
|
const clearAutoTimer = useCallback(() => {
|
|
550
562
|
if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
|
|
551
563
|
}, []);
|
|
552
564
|
|
|
553
|
-
// Start auto-advance timer for remaining duration
|
|
554
565
|
const startAutoTimer = useCallback((remainingMs: number) => {
|
|
555
566
|
clearAutoTimer();
|
|
556
567
|
timerRef.current = setTimeout(() => {
|
|
557
|
-
if (slideIdx <
|
|
568
|
+
if (slideIdx < totalSlides - 1) {
|
|
558
569
|
setDirection(1);
|
|
559
570
|
setSlideIdx(prev => prev + 1);
|
|
560
571
|
} else {
|
|
@@ -562,23 +573,22 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
562
573
|
setPaused(true);
|
|
563
574
|
}
|
|
564
575
|
}, remainingMs);
|
|
565
|
-
}, [slideIdx, clearAutoTimer]);
|
|
576
|
+
}, [slideIdx, totalSlides, clearAutoTimer]);
|
|
566
577
|
|
|
567
|
-
// Progress fill RAF — respects paused state on slide change
|
|
568
578
|
useEffect(() => {
|
|
569
579
|
setSlideFill(0);
|
|
570
580
|
pausedElapsedRef.current = 0;
|
|
571
581
|
slideStartRef.current = Date.now();
|
|
572
582
|
|
|
573
|
-
if (paused) return;
|
|
583
|
+
if (paused) return;
|
|
574
584
|
|
|
575
585
|
const tick = () => {
|
|
576
586
|
const elapsed = Date.now() - slideStartRef.current;
|
|
577
|
-
setSlideFill(Math.min(elapsed /
|
|
587
|
+
setSlideFill(Math.min(elapsed / slides[slideIdx].duration, 1));
|
|
578
588
|
rafRef.current = requestAnimationFrame(tick);
|
|
579
589
|
};
|
|
580
590
|
rafRef.current = requestAnimationFrame(tick);
|
|
581
|
-
startAutoTimer(
|
|
591
|
+
startAutoTimer(slides[slideIdx].duration);
|
|
582
592
|
|
|
583
593
|
return () => {
|
|
584
594
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -586,28 +596,25 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
586
596
|
};
|
|
587
597
|
}, [slideIdx]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
588
598
|
|
|
589
|
-
// Handle pause/resume
|
|
590
599
|
const togglePause = useCallback(() => {
|
|
591
600
|
if (paused) {
|
|
592
|
-
|
|
593
|
-
const remaining = SLIDE_DURATIONS[slideIdx] - pausedElapsedRef.current;
|
|
601
|
+
const remaining = slides[slideIdx].duration - pausedElapsedRef.current;
|
|
594
602
|
slideStartRef.current = Date.now() - pausedElapsedRef.current;
|
|
595
603
|
const tick = () => {
|
|
596
604
|
const elapsed = Date.now() - slideStartRef.current;
|
|
597
|
-
setSlideFill(Math.min(elapsed /
|
|
605
|
+
setSlideFill(Math.min(elapsed / slides[slideIdx].duration, 1));
|
|
598
606
|
rafRef.current = requestAnimationFrame(tick);
|
|
599
607
|
};
|
|
600
608
|
rafRef.current = requestAnimationFrame(tick);
|
|
601
609
|
startAutoTimer(remaining);
|
|
602
610
|
setPaused(false);
|
|
603
611
|
} else {
|
|
604
|
-
// Pause: freeze everything
|
|
605
612
|
cancelAnimationFrame(rafRef.current);
|
|
606
613
|
clearAutoTimer();
|
|
607
614
|
pausedElapsedRef.current = Date.now() - slideStartRef.current;
|
|
608
615
|
setPaused(true);
|
|
609
616
|
}
|
|
610
|
-
}, [paused, slideIdx, startAutoTimer, clearAutoTimer]);
|
|
617
|
+
}, [paused, slideIdx, slides, startAutoTimer, clearAutoTimer]);
|
|
611
618
|
|
|
612
619
|
const replay = useCallback(() => {
|
|
613
620
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -621,14 +628,14 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
621
628
|
const seekNext = useCallback(() => {
|
|
622
629
|
cancelAnimationFrame(rafRef.current);
|
|
623
630
|
clearAutoTimer();
|
|
624
|
-
if (slideIdx <
|
|
631
|
+
if (slideIdx < totalSlides - 1) {
|
|
625
632
|
setFinished(false);
|
|
626
633
|
setDirection(1);
|
|
627
634
|
setSlideIdx(prev => prev + 1);
|
|
628
635
|
} else {
|
|
629
|
-
|
|
636
|
+
onComplete();
|
|
630
637
|
}
|
|
631
|
-
}, [slideIdx,
|
|
638
|
+
}, [slideIdx, totalSlides, onComplete, clearAutoTimer]);
|
|
632
639
|
|
|
633
640
|
const seekBack = useCallback(() => {
|
|
634
641
|
cancelAnimationFrame(rafRef.current);
|
|
@@ -646,16 +653,16 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
646
653
|
exit: (dir: number) => ({ opacity: 0, x: dir * -60 }),
|
|
647
654
|
};
|
|
648
655
|
|
|
649
|
-
const
|
|
650
|
-
const
|
|
656
|
+
const { component: SlideComponent } = slides[slideIdx];
|
|
657
|
+
const isLastSlide = slideIdx === totalSlides - 1;
|
|
651
658
|
|
|
652
659
|
return (
|
|
653
660
|
<div className="relative w-full h-full flex flex-col bg-black">
|
|
654
661
|
{/* Skip button */}
|
|
655
662
|
<div className="flex justify-end px-4 pt-3">
|
|
656
663
|
<button
|
|
657
|
-
onClick={
|
|
658
|
-
className="text-[10px] text-white/25 uppercase tracking-wider font-semibold"
|
|
664
|
+
onClick={onComplete}
|
|
665
|
+
className="text-[10px] text-white/25 uppercase tracking-wider font-semibold cursor-pointer"
|
|
659
666
|
style={OUTFIT}
|
|
660
667
|
>
|
|
661
668
|
Skip
|
|
@@ -663,7 +670,7 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
663
670
|
</div>
|
|
664
671
|
|
|
665
672
|
{/* Slide content — centered */}
|
|
666
|
-
<div className="flex-1 flex items-center justify-center px-6">
|
|
673
|
+
<div className="flex-1 flex items-center justify-center px-6 overflow-y-auto">
|
|
667
674
|
<AnimatePresence mode="wait" custom={direction}>
|
|
668
675
|
<motion.div
|
|
669
676
|
key={slideIdx}
|
|
@@ -673,9 +680,9 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
673
680
|
animate="center"
|
|
674
681
|
exit="exit"
|
|
675
682
|
transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
|
|
676
|
-
className="w-full flex justify-center"
|
|
683
|
+
className="w-full flex justify-center py-4"
|
|
677
684
|
>
|
|
678
|
-
<SlideComponent config={config} />
|
|
685
|
+
<SlideComponent config={config} onComplete={onComplete} />
|
|
679
686
|
</motion.div>
|
|
680
687
|
</AnimatePresence>
|
|
681
688
|
</div>
|
|
@@ -687,7 +694,7 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
687
694
|
{finished ? (
|
|
688
695
|
<button
|
|
689
696
|
onClick={replay}
|
|
690
|
-
className="flex items-center gap-1.5 p-1.5"
|
|
697
|
+
className="flex items-center gap-1.5 p-1.5 cursor-pointer"
|
|
691
698
|
style={{ opacity: 0.5 }}
|
|
692
699
|
>
|
|
693
700
|
<RotateCcw size={16} className="text-white" />
|
|
@@ -698,23 +705,27 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
698
705
|
<button
|
|
699
706
|
onClick={seekBack}
|
|
700
707
|
disabled={slideIdx === 0}
|
|
701
|
-
className="p-1.5"
|
|
708
|
+
className="p-1.5 cursor-pointer"
|
|
702
709
|
style={{ opacity: slideIdx === 0 ? 0.15 : 0.5 }}
|
|
703
710
|
>
|
|
704
711
|
<Rewind size={18} className="text-white" />
|
|
705
712
|
</button>
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
713
|
+
{/* Hide play/pause on last slide — user should use Build Your Bets button */}
|
|
714
|
+
{!isLastSlide && (
|
|
715
|
+
<button
|
|
716
|
+
onClick={togglePause}
|
|
717
|
+
className="p-1.5 cursor-pointer"
|
|
718
|
+
>
|
|
719
|
+
{paused
|
|
720
|
+
? <svg width="18" height="18" viewBox="0 0 18 18" fill="none"><polygon points="5,3 15,9 5,15" fill="white" style={{ marginLeft: 2 }} /></svg>
|
|
721
|
+
: <Pause size={18} className="text-white" />
|
|
722
|
+
}
|
|
723
|
+
</button>
|
|
724
|
+
)}
|
|
725
|
+
{isLastSlide && <div style={{ width: 18 + 12 }} />}
|
|
715
726
|
<button
|
|
716
727
|
onClick={seekNext}
|
|
717
|
-
className="p-1.5"
|
|
728
|
+
className="p-1.5 cursor-pointer"
|
|
718
729
|
style={{ opacity: 0.5 }}
|
|
719
730
|
>
|
|
720
731
|
<FastForward size={18} className="text-white" />
|
|
@@ -723,14 +734,14 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
723
734
|
)}
|
|
724
735
|
</div>
|
|
725
736
|
|
|
726
|
-
{/* Segmented progress bar
|
|
737
|
+
{/* Segmented progress bar */}
|
|
727
738
|
<div style={{ display: "flex", alignItems: "center", width: "100%", gap: 6 }}>
|
|
728
|
-
{Array.from({ length:
|
|
739
|
+
{Array.from({ length: totalSlides }).map((_, i) => {
|
|
729
740
|
const filled = i < slideIdx;
|
|
730
741
|
const active = i === slideIdx;
|
|
731
742
|
const passed = i <= slideIdx;
|
|
732
743
|
return (
|
|
733
|
-
<div key={i} style={{ display: "flex", alignItems: "center", flex: i <
|
|
744
|
+
<div key={i} style={{ display: "flex", alignItems: "center", flex: i < totalSlides - 1 ? 1 : "none", gap: 6 }}>
|
|
734
745
|
<div
|
|
735
746
|
style={{
|
|
736
747
|
width: 7, height: 7, borderRadius: "50%", flexShrink: 0,
|
|
@@ -738,7 +749,7 @@ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
|
|
|
738
749
|
boxShadow: passed ? "0 0 6px rgba(34,227,232,0.4)" : "none",
|
|
739
750
|
}}
|
|
740
751
|
/>
|
|
741
|
-
{i <
|
|
752
|
+
{i < totalSlides - 1 && (
|
|
742
753
|
<div style={{ flex: 1, height: 3, borderRadius: 2, background: "rgba(255,255,255,0.08)", overflow: "hidden" }}>
|
|
743
754
|
<div
|
|
744
755
|
style={{
|
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
|
|