@devrongx/games 0.1.0

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.
@@ -0,0 +1,759 @@
1
+ // @devrongx/games — games/prematch-bets/PreMatchIntro.tsx
2
+ "use client";
3
+
4
+ import { useState, useEffect, useCallback, useRef } from "react";
5
+ import Image from "next/image";
6
+ import { motion, AnimatePresence } from "framer-motion";
7
+ import { Coins, Trophy, Play, Pause, Rewind, FastForward, RotateCcw } from "lucide-react";
8
+ import { useGamePopupStore } from "../../core/gamePopupStore";
9
+ import { IChallengeConfig } from "./config";
10
+
11
+ const OUTFIT = { fontFamily: "Outfit, sans-serif" };
12
+ const TOTAL_SLIDES = 6;
13
+ const SLIDE_DURATIONS = [5500, 5000, 5500, 7000, 5500, 5500];
14
+
15
+ // Countdown hook for match start
16
+ const useCountdown = (targetIso: string) => {
17
+ const [text, setText] = useState("");
18
+ useEffect(() => {
19
+ const update = () => {
20
+ const diff = new Date(targetIso).getTime() - Date.now();
21
+ if (diff <= 0) { setText("LIVE NOW"); return; }
22
+ const d = Math.floor(diff / 86400000);
23
+ const h = Math.floor((diff % 86400000) / 3600000);
24
+ const m = Math.floor((diff % 3600000) / 60000);
25
+ const s = Math.floor((diff % 60000) / 1000);
26
+ if (d > 0) setText(`${d}d ${h}h ${m}m`);
27
+ else if (h > 0) setText(`${h}h ${m}m ${s}s`);
28
+ else setText(`${m}m ${s}s`);
29
+ };
30
+ update();
31
+ const id = setInterval(update, 1000);
32
+ return () => clearInterval(id);
33
+ }, [targetIso]);
34
+ return text;
35
+ };
36
+
37
+ // Slide props — all slides receive config
38
+ interface SlideProps { config: IChallengeConfig }
39
+
40
+ // ── Slide 0: Logo reveal + match info + countdown ──
41
+ const LogoSlide = ({ config }: SlideProps) => {
42
+ const countdown = useCountdown(config.matchStartTime);
43
+ const matchDate = new Date(config.matchStartTime);
44
+ const dateStr = matchDate.toLocaleDateString("en-IN", { day: "numeric", month: "short" });
45
+ const timeStr = matchDate.toLocaleTimeString("en-IN", { hour: "numeric", minute: "2-digit", hour12: true });
46
+
47
+ return (
48
+ <motion.div
49
+ className="flex flex-col items-center gap-5"
50
+ initial="initial"
51
+ animate="animate"
52
+ exit="exit"
53
+ >
54
+ {/* Logo — appears immediately */}
55
+ <motion.div
56
+ initial={{ opacity: 0, y: 24 }}
57
+ animate={{ opacity: 1, y: 0 }}
58
+ transition={{ duration: 0.6, ease: "easeOut" }}
59
+ className="w-20 h-20 rounded-2xl flex items-center justify-center"
60
+ style={{ background: "linear-gradient(135deg, rgba(34,227,232,0.15), rgba(153,69,255,0.15))" }}
61
+ >
62
+ <svg width="44" height="44" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
63
+ <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.87268 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
+ <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
+ <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
+ </g>
68
+ <defs>
69
+ <linearGradient id="paint0_intro_logo" x1="25.9471" y1="9.88881" x2="2.03225" y2="9.7064" gradientUnits="userSpaceOnUse">
70
+ <stop stopColor="#22E3E8" />
71
+ <stop offset="1" stopColor="#9945FF" />
72
+ </linearGradient>
73
+ <linearGradient id="paint1_intro_logo" x1="28" y1="11.055" x2="0" y2="10.5681" gradientUnits="userSpaceOnUse">
74
+ <stop stopColor="#22E3E8" />
75
+ <stop offset="1" stopColor="#9945FF" />
76
+ </linearGradient>
77
+ <linearGradient id="paint2_intro_logo" x1="28" y1="20.8916" x2="0" y2="20.6055" gradientUnits="userSpaceOnUse">
78
+ <stop stopColor="#22E3E8" />
79
+ <stop offset="1" stopColor="#9945FF" />
80
+ </linearGradient>
81
+ <clipPath id="clip_intro_logo">
82
+ <rect width="28" height="28" fill="white" />
83
+ </clipPath>
84
+ </defs>
85
+ </svg>
86
+ </motion.div>
87
+ {/* Match info — appears after logo settles */}
88
+ <motion.div
89
+ initial={{ opacity: 0, y: 20 }}
90
+ animate={{ opacity: 1, y: 0 }}
91
+ transition={{ duration: 0.5, delay: 0.8 }}
92
+ className="flex flex-col items-center gap-1"
93
+ >
94
+ <p className="text-[28px] font-bold text-white tracking-tight leading-none text-center" style={OUTFIT}>
95
+ {config.teamA.name} vs {config.teamB.name}
96
+ </p>
97
+ <p className="text-[14px] text-white font-medium tracking-wide" style={OUTFIT}>
98
+ {dateStr} &middot; {timeStr}
99
+ </p>
100
+ </motion.div>
101
+ {/* Countdown — appears after match info is read */}
102
+ <motion.div
103
+ initial={{ opacity: 0, y: 20 }}
104
+ animate={{ opacity: 1, y: 0 }}
105
+ transition={{ duration: 0.5, delay: 1.8 }}
106
+ className="flex flex-col items-center gap-1"
107
+ >
108
+ <span className="text-[10px] text-white/25 uppercase tracking-widest font-semibold" style={OUTFIT}>Starts in</span>
109
+ <span
110
+ className="text-[20px] font-bold leading-none"
111
+ style={{
112
+ ...OUTFIT,
113
+ background: "linear-gradient(90deg, #22E3E8, #9945FF)",
114
+ WebkitBackgroundClip: "text",
115
+ WebkitTextFillColor: "transparent",
116
+ backgroundClip: "text",
117
+ }}
118
+ >
119
+ {countdown}
120
+ </span>
121
+ </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
+ </motion.div>
142
+ );
143
+ };
144
+
145
+ // ── Slide 1: Buy in with a match pass ──
146
+ const EntrySlide = ({ config }: SlideProps) => (
147
+ <motion.div
148
+ className="flex flex-col items-center gap-4"
149
+ initial="initial"
150
+ animate="animate"
151
+ exit="exit"
152
+ >
153
+ {/* Headline — appears first */}
154
+ <motion.p
155
+ initial={{ opacity: 0, y: 20 }}
156
+ animate={{ opacity: 1, y: 0 }}
157
+ transition={{ duration: 0.5, delay: 0.2 }}
158
+ className="text-[15px] text-white font-medium"
159
+ style={OUTFIT}
160
+ >
161
+ Buy in with a match pass
162
+ </motion.p>
163
+ {/* Price + USDC — appears after reading the headline */}
164
+ <motion.div
165
+ initial={{ opacity: 0, y: 20 }}
166
+ animate={{ opacity: 1, y: 0 }}
167
+ transition={{ duration: 0.6, delay: 1.2 }}
168
+ className="flex items-center gap-3"
169
+ >
170
+ <span className="text-[52px] font-bold text-white leading-none" style={OUTFIT}>${config.entryFee}</span>
171
+ <Image src="/icons/ic_usdc_hd.png" alt="USDC" width={52} height={52} className="rounded-full" />
172
+ </motion.div>
173
+ {/* Subtitle — appears last */}
174
+ <motion.p
175
+ initial={{ opacity: 0, y: 16 }}
176
+ animate={{ opacity: 1, y: 0 }}
177
+ transition={{ duration: 0.5, delay: 2.2 }}
178
+ className="text-[14px] text-white/50 font-medium"
179
+ style={OUTFIT}
180
+ >
181
+ to enter the arena
182
+ </motion.p>
183
+ </motion.div>
184
+ );
185
+
186
+ // ── Slide 2: Get 1,000 points ──
187
+ const PointsSlide = ({ config }: SlideProps) => (
188
+ <motion.div
189
+ className="flex flex-col items-center"
190
+ initial="initial"
191
+ animate="animate"
192
+ exit="exit"
193
+ >
194
+ {/* "You get" — appears first */}
195
+ <motion.p
196
+ initial={{ opacity: 0, y: 20 }}
197
+ animate={{ opacity: 1, y: 0 }}
198
+ transition={{ duration: 0.5, delay: 0.2 }}
199
+ className="text-[15px] text-white font-medium mb-2"
200
+ style={OUTFIT}
201
+ >
202
+ You get
203
+ </motion.p>
204
+ {/* Big number + logo — appears after "You get" is read */}
205
+ <motion.div
206
+ initial={{ opacity: 0, y: 20 }}
207
+ animate={{ opacity: 1, y: 0 }}
208
+ transition={{ duration: 0.6, delay: 1.0 }}
209
+ className="flex items-center gap-3"
210
+ >
211
+ <Image src="/iamgame_square_logo.jpg" alt="" width={52} height={52} className="rounded-[8px]" />
212
+ <span
213
+ className="text-[56px] font-bold leading-none"
214
+ style={{
215
+ ...OUTFIT,
216
+ background: "linear-gradient(135deg, #22E3E8, #9945FF)",
217
+ WebkitBackgroundClip: "text",
218
+ WebkitTextFillColor: "transparent",
219
+ backgroundClip: "text",
220
+ }}
221
+ >
222
+ {config.startingBalance.toLocaleString()}
223
+ </span>
224
+ </motion.div>
225
+ {/* "prediction points" — appears after the number lands */}
226
+ <motion.p
227
+ initial={{ opacity: 0, y: 16 }}
228
+ animate={{ opacity: 1, y: 0 }}
229
+ transition={{ duration: 0.5, delay: 2.0 }}
230
+ className="text-[15px] text-white font-medium mt-2"
231
+ style={OUTFIT}
232
+ >
233
+ prediction points to bet with
234
+ </motion.p>
235
+ {/* Delayed tip — appears last */}
236
+ <motion.p
237
+ initial={{ opacity: 0, y: 10 }}
238
+ animate={{ opacity: 1, y: 0 }}
239
+ transition={{ duration: 0.5, delay: 3.2 }}
240
+ className="text-[13px] text-white/60 font-medium text-center mt-4"
241
+ style={OUTFIT}
242
+ >
243
+ Participate in bets to maximize your points
244
+ </motion.p>
245
+ </motion.div>
246
+ );
247
+
248
+ // ── Slide 3: Markets demo ──
249
+ const MarketsSlide = ({ config }: SlideProps) => {
250
+ const [demoStep, setDemoStep] = useState(0);
251
+
252
+ useEffect(() => {
253
+ const timers = [
254
+ setTimeout(() => setDemoStep(1), 2800), // option highlights — after card appears
255
+ setTimeout(() => setDemoStep(2), 4200), // chip selected
256
+ ];
257
+ return () => timers.forEach(clearTimeout);
258
+ }, []);
259
+
260
+ return (
261
+ <motion.div
262
+ className="flex flex-col items-center gap-5 w-full max-w-[300px]"
263
+ initial="initial"
264
+ animate="animate"
265
+ exit="exit"
266
+ >
267
+ {/* Headline — appears first */}
268
+ <motion.p
269
+ initial={{ opacity: 0, y: 20 }}
270
+ animate={{ opacity: 1, y: 0 }}
271
+ transition={{ duration: 0.5, delay: 0.2 }}
272
+ className="text-[14px] text-white font-medium text-center"
273
+ style={OUTFIT}
274
+ >
275
+ {config.markets.length} markets to predict
276
+ </motion.p>
277
+
278
+ {/* Floating market card — appears after headline is read */}
279
+ <motion.div
280
+ initial={{ opacity: 0, y: 20 }}
281
+ animate={{ opacity: 1, y: 0 }}
282
+ transition={{ duration: 0.6, delay: 1.2 }}
283
+ className="w-full rounded-lg px-3 py-3"
284
+ style={{
285
+ background: "rgba(255,255,255,0.03)",
286
+ border: "1px solid rgba(255,255,255,0.08)",
287
+ }}
288
+ >
289
+ {/* Market question */}
290
+ <div className="flex items-center gap-2 mb-2.5">
291
+ <Coins size={14} className="text-[#22E3E8]/50 flex-shrink-0" />
292
+ <span className="text-[13px] text-white font-semibold" style={OUTFIT}>
293
+ Who wins the toss?
294
+ </span>
295
+ </div>
296
+ {/* Options */}
297
+ <div className="grid grid-cols-2 gap-2">
298
+ {/* CSK option — gets selected */}
299
+ <div
300
+ className="flex items-center justify-between px-2.5 py-2 rounded-sm transition-all duration-500"
301
+ style={{
302
+ background: demoStep >= 1
303
+ ? "linear-gradient(135deg, #22E3E8, #9945FF)"
304
+ : "transparent",
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)",
311
+ borderTop: "1px solid transparent",
312
+ borderRight: "1px solid transparent",
313
+ }}
314
+ >
315
+ <div className="flex items-center gap-1.5">
316
+ {demoStep >= 1 && (
317
+ <motion.svg
318
+ initial={{ scale: 0 }}
319
+ animate={{ scale: 1 }}
320
+ transition={{ type: "spring", stiffness: 400, damping: 15 }}
321
+ width="10" height="10" viewBox="0 0 16 16" fill="none"
322
+ >
323
+ <circle cx="8" cy="8" r="8" fill="white" />
324
+ <path d="M5 8l2 2 4-4" stroke="#9945FF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
325
+ </motion.svg>
326
+ )}
327
+ <span className="text-[11px] text-white font-semibold" style={OUTFIT}>{config.teamA.name}</span>
328
+ </div>
329
+ <span className={`text-[10px] font-bold ${demoStep >= 1 ? "text-white" : "text-[#22E3E8]"}`} style={OUTFIT}>
330
+ 1.4x
331
+ </span>
332
+ </div>
333
+ {/* RCB option — stays default */}
334
+ <div
335
+ className="flex items-center justify-between px-2.5 py-2 rounded-sm"
336
+ style={{
337
+ borderLeft: "1px solid rgba(255,255,255,0.12)",
338
+ borderBottom: "1px solid rgba(255,255,255,0.06)",
339
+ borderTop: "1px solid transparent",
340
+ borderRight: "1px solid transparent",
341
+ }}
342
+ >
343
+ <span className="text-[11px] text-white font-semibold" style={OUTFIT}>{config.teamB.name}</span>
344
+ <span className="text-[10px] font-bold text-[#22E3E8]" style={OUTFIT}>1.4x</span>
345
+ </div>
346
+ </div>
347
+ {/* Amount picker — appears at step 2 */}
348
+ <AnimatePresence>
349
+ {demoStep >= 2 && (
350
+ <motion.div
351
+ initial={{ opacity: 0, height: 0 }}
352
+ animate={{ opacity: 1, height: "auto" }}
353
+ exit={{ opacity: 0, height: 0 }}
354
+ transition={{ duration: 0.3 }}
355
+ className="mt-2 flex items-center gap-1.5 overflow-hidden"
356
+ >
357
+ <span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
358
+ {[50, 100, 150, 200].map((amt, i) => (
359
+ <motion.div
360
+ key={amt}
361
+ initial={{ opacity: 0, scale: 0.8 }}
362
+ animate={{ opacity: 1, scale: 1 }}
363
+ transition={{ delay: i * 0.08 }}
364
+ className="flex items-center gap-0.5 px-2 py-[3px] rounded relative overflow-hidden"
365
+ style={amt === 100 ? {
366
+ background: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)",
367
+ } : {
368
+ background: "rgba(34,227,232,0.08)",
369
+ border: "1px solid rgba(34,227,232,0.2)",
370
+ }}
371
+ >
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
+ <Image src="/iamgame_square_logo.jpg" alt="" width={7} height={7} className="rounded-[1px] relative z-[1]" />
382
+ <span
383
+ className={`text-[9px] font-bold relative z-[1] ${amt === 100 ? "text-black" : "text-[#22E3E8]"}`}
384
+ style={OUTFIT}
385
+ >
386
+ {amt}
387
+ </span>
388
+ </motion.div>
389
+ ))}
390
+ </motion.div>
391
+ )}
392
+ </AnimatePresence>
393
+ </motion.div>
394
+
395
+ {/* Subtitle — appears after demo plays */}
396
+ <motion.p
397
+ initial={{ opacity: 0, y: 16 }}
398
+ animate={{ opacity: 1, y: 0 }}
399
+ transition={{ duration: 0.5, delay: 5.0 }}
400
+ className="text-[12px] text-white/40 font-medium text-center"
401
+ style={OUTFIT}
402
+ >
403
+ Toss, winner, top scorers, boundaries, wickets & more
404
+ </motion.p>
405
+ </motion.div>
406
+ );
407
+ };
408
+
409
+ // ── Slide 4: Compound multiplier ──
410
+ const MultiplierSlide = ({ config }: SlideProps) => (
411
+ <motion.div
412
+ className="flex flex-col items-center gap-4"
413
+ initial="initial"
414
+ animate="animate"
415
+ exit="exit"
416
+ >
417
+ {/* First line — appears immediately */}
418
+ <motion.p
419
+ initial={{ opacity: 0, y: 20 }}
420
+ animate={{ opacity: 1, y: 0 }}
421
+ transition={{ duration: 0.5, delay: 0.2 }}
422
+ className="text-[15px] text-white font-semibold"
423
+ style={OUTFIT}
424
+ >
425
+ Stack predictions.
426
+ </motion.p>
427
+ {/* Second line — after first is read */}
428
+ <motion.p
429
+ initial={{ opacity: 0, y: 20 }}
430
+ animate={{ opacity: 1, y: 0 }}
431
+ transition={{ duration: 0.5, delay: 1.0 }}
432
+ className="text-[15px] text-white/70 font-medium"
433
+ style={OUTFIT}
434
+ >
435
+ Multiply your odds.
436
+ </motion.p>
437
+ {/* Big number — the reveal moment */}
438
+ <motion.div
439
+ initial={{ opacity: 0, scale: 0.8 }}
440
+ animate={{ opacity: 1, scale: 1 }}
441
+ transition={{ duration: 0.7, delay: 2.0, ease: "easeOut" }}
442
+ >
443
+ <span
444
+ className="text-[72px] font-bold leading-none"
445
+ style={{
446
+ ...OUTFIT,
447
+ background: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)",
448
+ WebkitBackgroundClip: "text",
449
+ WebkitTextFillColor: "transparent",
450
+ backgroundClip: "text",
451
+ }}
452
+ >
453
+ {config.compoundMultipliers[config.compoundMultipliers.length - 1]}x
454
+ </span>
455
+ </motion.div>
456
+ {/* Label — after number lands */}
457
+ <motion.p
458
+ initial={{ opacity: 0, y: 16 }}
459
+ animate={{ opacity: 1, y: 0 }}
460
+ transition={{ duration: 0.5, delay: 3.0 }}
461
+ className="text-[12px] text-white/40 font-medium"
462
+ style={OUTFIT}
463
+ >
464
+ max compound multiplier
465
+ </motion.p>
466
+ </motion.div>
467
+ );
468
+
469
+ // ── Slide 5: Win USDC ──
470
+ const WinSlide = ({ config: _config }: SlideProps) => {
471
+ const goTo = useGamePopupStore(s => s.goTo);
472
+ return (
473
+ <motion.div
474
+ className="flex flex-col items-center gap-4"
475
+ initial="initial"
476
+ animate="animate"
477
+ exit="exit"
478
+ >
479
+ {/* Trophy — appears first */}
480
+ <motion.div
481
+ initial={{ opacity: 0, y: 20 }}
482
+ animate={{ opacity: 1, y: 0 }}
483
+ transition={{ duration: 0.5, delay: 0.2 }}
484
+ >
485
+ <Trophy size={36} className="text-[#FFD700]" />
486
+ </motion.div>
487
+ {/* Headline — after trophy */}
488
+ <motion.p
489
+ initial={{ opacity: 0, y: 20 }}
490
+ animate={{ opacity: 1, y: 0 }}
491
+ transition={{ duration: 0.5, delay: 0.8 }}
492
+ className="text-[18px] text-white font-bold"
493
+ style={OUTFIT}
494
+ >
495
+ Top the leaderboard.
496
+ </motion.p>
497
+ {/* Win real USDC — after headline is read */}
498
+ <motion.div
499
+ initial={{ opacity: 0, y: 20 }}
500
+ animate={{ opacity: 1, y: 0 }}
501
+ transition={{ duration: 0.6, delay: 1.8 }}
502
+ className="flex items-center gap-2"
503
+ >
504
+ <span className="text-[18px] text-white/70 font-medium" style={OUTFIT}>Win real</span>
505
+ <Image src="/icons/ic_usdc_hd.png" alt="USDC" width={22} height={22} className="rounded-full" />
506
+ <span className="text-[18px] text-[#2775CA] font-bold" style={OUTFIT}>USDC</span>
507
+ </motion.div>
508
+ {/* Tagline — after USDC line */}
509
+ <motion.p
510
+ initial={{ opacity: 0, y: 16 }}
511
+ animate={{ opacity: 1, y: 0 }}
512
+ transition={{ duration: 0.5, delay: 2.8 }}
513
+ className="text-[13px] text-white/40 font-medium"
514
+ style={OUTFIT}
515
+ >
516
+ Take the money home.
517
+ </motion.p>
518
+ {/* CTA button — appears last */}
519
+ <motion.button
520
+ initial={{ opacity: 0, y: 16 }}
521
+ animate={{ opacity: 1, y: 0 }}
522
+ transition={{ duration: 0.5, delay: 3.5 }}
523
+ onClick={() => goTo("questions")}
524
+ className="mt-4 px-6 py-2.5 rounded-full cursor-pointer"
525
+ style={{ background: "linear-gradient(135deg, #22E3E8, #9945FF)" }}
526
+ >
527
+ <span className="text-[14px] font-bold text-black" style={OUTFIT}>Build Your Bets</span>
528
+ </motion.button>
529
+ </motion.div>
530
+ );
531
+ };
532
+
533
+
534
+ export const PreMatchIntro = ({ config }: { config: IChallengeConfig }) => {
535
+ const goTo = useGamePopupStore(s => s.goTo);
536
+ const [slideIdx, setSlideIdx] = useState(0);
537
+ const [direction, setDirection] = useState(1);
538
+ const [paused, setPaused] = useState(false);
539
+ const [slideFill, setSlideFill] = useState(0);
540
+ const [finished, setFinished] = useState(false);
541
+
542
+ // Refs for timing
543
+ const slideStartRef = useRef(Date.now());
544
+ const pausedElapsedRef = useRef(0); // how much time had elapsed when paused
545
+ const rafRef = useRef<number>(0);
546
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
547
+
548
+ // Clear auto-advance timer
549
+ const clearAutoTimer = useCallback(() => {
550
+ if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
551
+ }, []);
552
+
553
+ // Start auto-advance timer for remaining duration
554
+ const startAutoTimer = useCallback((remainingMs: number) => {
555
+ clearAutoTimer();
556
+ timerRef.current = setTimeout(() => {
557
+ if (slideIdx < TOTAL_SLIDES - 1) {
558
+ setDirection(1);
559
+ setSlideIdx(prev => prev + 1);
560
+ } else {
561
+ setFinished(true);
562
+ setPaused(true);
563
+ }
564
+ }, remainingMs);
565
+ }, [slideIdx, clearAutoTimer]);
566
+
567
+ // Progress fill RAF — respects paused state on slide change
568
+ useEffect(() => {
569
+ setSlideFill(0);
570
+ pausedElapsedRef.current = 0;
571
+ slideStartRef.current = Date.now();
572
+
573
+ if (paused) return; // stay paused at 0 fill on new slide
574
+
575
+ const tick = () => {
576
+ const elapsed = Date.now() - slideStartRef.current;
577
+ setSlideFill(Math.min(elapsed / SLIDE_DURATIONS[slideIdx], 1));
578
+ rafRef.current = requestAnimationFrame(tick);
579
+ };
580
+ rafRef.current = requestAnimationFrame(tick);
581
+ startAutoTimer(SLIDE_DURATIONS[slideIdx]);
582
+
583
+ return () => {
584
+ cancelAnimationFrame(rafRef.current);
585
+ clearAutoTimer();
586
+ };
587
+ }, [slideIdx]); // eslint-disable-line react-hooks/exhaustive-deps
588
+
589
+ // Handle pause/resume
590
+ const togglePause = useCallback(() => {
591
+ if (paused) {
592
+ // Resume: restart RAF and timer from where we left off
593
+ const remaining = SLIDE_DURATIONS[slideIdx] - pausedElapsedRef.current;
594
+ slideStartRef.current = Date.now() - pausedElapsedRef.current;
595
+ const tick = () => {
596
+ const elapsed = Date.now() - slideStartRef.current;
597
+ setSlideFill(Math.min(elapsed / SLIDE_DURATIONS[slideIdx], 1));
598
+ rafRef.current = requestAnimationFrame(tick);
599
+ };
600
+ rafRef.current = requestAnimationFrame(tick);
601
+ startAutoTimer(remaining);
602
+ setPaused(false);
603
+ } else {
604
+ // Pause: freeze everything
605
+ cancelAnimationFrame(rafRef.current);
606
+ clearAutoTimer();
607
+ pausedElapsedRef.current = Date.now() - slideStartRef.current;
608
+ setPaused(true);
609
+ }
610
+ }, [paused, slideIdx, startAutoTimer, clearAutoTimer]);
611
+
612
+ const replay = useCallback(() => {
613
+ cancelAnimationFrame(rafRef.current);
614
+ clearAutoTimer();
615
+ setFinished(false);
616
+ setPaused(false);
617
+ setDirection(-1);
618
+ setSlideIdx(0);
619
+ }, [clearAutoTimer]);
620
+
621
+ const seekNext = useCallback(() => {
622
+ cancelAnimationFrame(rafRef.current);
623
+ clearAutoTimer();
624
+ if (slideIdx < TOTAL_SLIDES - 1) {
625
+ setFinished(false);
626
+ setDirection(1);
627
+ setSlideIdx(prev => prev + 1);
628
+ } else {
629
+ goTo("questions");
630
+ }
631
+ }, [slideIdx, goTo, clearAutoTimer]);
632
+
633
+ const seekBack = useCallback(() => {
634
+ cancelAnimationFrame(rafRef.current);
635
+ clearAutoTimer();
636
+ setFinished(false);
637
+ if (slideIdx > 0) {
638
+ setDirection(-1);
639
+ setSlideIdx(prev => prev - 1);
640
+ }
641
+ }, [slideIdx, clearAutoTimer]);
642
+
643
+ const slideVariants = {
644
+ enter: (dir: number) => ({ opacity: 0, x: dir * 60 }),
645
+ center: { opacity: 1, x: 0 },
646
+ exit: (dir: number) => ({ opacity: 0, x: dir * -60 }),
647
+ };
648
+
649
+ const SLIDES = [LogoSlide, EntrySlide, PointsSlide, MarketsSlide, MultiplierSlide, WinSlide] as const;
650
+ const SlideComponent = SLIDES[slideIdx] as React.ComponentType<SlideProps>;
651
+
652
+ return (
653
+ <div className="relative w-full h-full flex flex-col bg-black">
654
+ {/* Skip button */}
655
+ <div className="flex justify-end px-4 pt-3">
656
+ <button
657
+ onClick={() => goTo("questions")}
658
+ className="text-[10px] text-white/25 uppercase tracking-wider font-semibold"
659
+ style={OUTFIT}
660
+ >
661
+ Skip
662
+ </button>
663
+ </div>
664
+
665
+ {/* Slide content — centered */}
666
+ <div className="flex-1 flex items-center justify-center px-6">
667
+ <AnimatePresence mode="wait" custom={direction}>
668
+ <motion.div
669
+ key={slideIdx}
670
+ custom={direction}
671
+ variants={slideVariants}
672
+ initial="enter"
673
+ animate="center"
674
+ exit="exit"
675
+ transition={{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
676
+ className="w-full flex justify-center"
677
+ >
678
+ <SlideComponent config={config} />
679
+ </motion.div>
680
+ </AnimatePresence>
681
+ </div>
682
+
683
+ {/* Bottom controls */}
684
+ <div className="px-5 pb-24 flex flex-col gap-3">
685
+ {/* Playback controls */}
686
+ <div className="flex items-center justify-center gap-6">
687
+ {finished ? (
688
+ <button
689
+ onClick={replay}
690
+ className="flex items-center gap-1.5 p-1.5"
691
+ style={{ opacity: 0.5 }}
692
+ >
693
+ <RotateCcw size={16} className="text-white" />
694
+ <span className="text-[11px] font-semibold text-white" style={OUTFIT}>Replay</span>
695
+ </button>
696
+ ) : (
697
+ <>
698
+ <button
699
+ onClick={seekBack}
700
+ disabled={slideIdx === 0}
701
+ className="p-1.5"
702
+ style={{ opacity: slideIdx === 0 ? 0.15 : 0.5 }}
703
+ >
704
+ <Rewind size={18} className="text-white" />
705
+ </button>
706
+ <button
707
+ onClick={togglePause}
708
+ className="p-1.5"
709
+ >
710
+ {paused
711
+ ? <Play size={18} className="text-white" style={{ marginLeft: 2 }} />
712
+ : <Pause size={18} className="text-white" />
713
+ }
714
+ </button>
715
+ <button
716
+ onClick={seekNext}
717
+ className="p-1.5"
718
+ style={{ opacity: 0.5 }}
719
+ >
720
+ <FastForward size={18} className="text-white" />
721
+ </button>
722
+ </>
723
+ )}
724
+ </div>
725
+
726
+ {/* Segmented progress bar with checkpoint circles */}
727
+ <div style={{ display: "flex", alignItems: "center", width: "100%", gap: 6 }}>
728
+ {Array.from({ length: TOTAL_SLIDES }).map((_, i) => {
729
+ const filled = i < slideIdx;
730
+ const active = i === slideIdx;
731
+ const passed = i <= slideIdx;
732
+ return (
733
+ <div key={i} style={{ display: "flex", alignItems: "center", flex: i < TOTAL_SLIDES - 1 ? 1 : "none", gap: 6 }}>
734
+ <div
735
+ style={{
736
+ width: 7, height: 7, borderRadius: "50%", flexShrink: 0,
737
+ background: passed ? "linear-gradient(135deg, #22E3E8, #9945FF)" : "rgba(255,255,255,0.12)",
738
+ boxShadow: passed ? "0 0 6px rgba(34,227,232,0.4)" : "none",
739
+ }}
740
+ />
741
+ {i < TOTAL_SLIDES - 1 && (
742
+ <div style={{ flex: 1, height: 3, borderRadius: 2, background: "rgba(255,255,255,0.08)", overflow: "hidden" }}>
743
+ <div
744
+ style={{
745
+ height: "100%", borderRadius: 2,
746
+ width: filled ? "100%" : active ? `${slideFill * 100}%` : "0%",
747
+ background: "linear-gradient(90deg, #22E3E8, #9945FF)",
748
+ }}
749
+ />
750
+ </div>
751
+ )}
752
+ </div>
753
+ );
754
+ })}
755
+ </div>
756
+ </div>
757
+ </div>
758
+ );
759
+ };