@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.
- package/package.json +33 -0
- package/src/core/GamePopupShell.tsx +67 -0
- package/src/core/gamePopupStore.ts +30 -0
- package/src/core/types.ts +20 -0
- package/src/games/prematch-bets/LeaderboardRow.tsx +67 -0
- package/src/games/prematch-bets/PreMatchBetsPopup.tsx +97 -0
- package/src/games/prematch-bets/PreMatchGame.tsx +829 -0
- package/src/games/prematch-bets/PreMatchIntro.tsx +759 -0
- package/src/games/prematch-bets/PreMatchQuestions.tsx +491 -0
- package/src/games/prematch-bets/config.ts +604 -0
- package/src/games/prematch-bets/constants.tsx +33 -0
- package/src/games/prematch-bets/index.ts +4 -0
- package/src/index.ts +10 -0
- package/src/styles/animations.css +16 -0
|
@@ -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} · {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
|
+
};
|