@devrongx/games 0.4.33 → 0.4.35
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 +1 -1
- package/src/games/prematch-bets/BetCelebration.tsx +88 -89
- package/src/games/prematch-bets/FullLeaderboard.tsx +255 -132
- package/src/games/prematch-bets/LeaderboardRow.tsx +28 -25
- package/src/games/prematch-bets/PreMatchBetsPopup.tsx +3 -2
- package/src/games/prematch-bets/PreMatchGame.tsx +4 -5
package/package.json
CHANGED
|
@@ -16,10 +16,10 @@ const Particle = ({ delay, angle, distance, size, color }: { delay: number; angl
|
|
|
16
16
|
return (
|
|
17
17
|
<motion.div
|
|
18
18
|
className="absolute rounded-full"
|
|
19
|
-
style={{ width: size, height: size, background: color, left: "50%", top: "
|
|
19
|
+
style={{ width: size, height: size, background: color, left: "50%", top: "35%", boxShadow: `0 0 ${size * 3}px ${color}` }}
|
|
20
20
|
initial={{ x: 0, y: 0, opacity: 0, scale: 0 }}
|
|
21
|
-
animate={{ x, y: y -
|
|
22
|
-
transition={{ duration: 1.
|
|
21
|
+
animate={{ x, y: y - 30, opacity: [0, 1, 0.8, 0], scale: [0, 1.8, 0.8, 0] }}
|
|
22
|
+
transition={{ duration: 1.6, delay, ease: "easeOut" }}
|
|
23
23
|
/>
|
|
24
24
|
);
|
|
25
25
|
};
|
|
@@ -27,26 +27,36 @@ const Particle = ({ delay, angle, distance, size, color }: { delay: number; angl
|
|
|
27
27
|
// ─── Animated SVG checkmark ──────────────────────────────────────────────────
|
|
28
28
|
|
|
29
29
|
const AnimatedCheck = ({ delay = 0, size = 56 }: { delay?: number; size?: number }) => (
|
|
30
|
-
<motion.
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
<motion.div
|
|
31
|
+
initial={{ scale: 0 }}
|
|
32
|
+
animate={{ scale: 1 }}
|
|
33
33
|
transition={{ type: "spring", stiffness: 260, damping: 14, delay }}
|
|
34
34
|
>
|
|
35
|
-
<motion.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
initial={{
|
|
39
|
-
animate={{
|
|
40
|
-
transition={{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
<motion.div
|
|
36
|
+
className="rounded-full flex items-center justify-center"
|
|
37
|
+
style={{ width: size, height: size, boxShadow: "0 0 40px rgba(34,227,232,0.3), 0 0 80px rgba(34,227,232,0.1)" }}
|
|
38
|
+
initial={{ opacity: 0 }}
|
|
39
|
+
animate={{ opacity: 1 }}
|
|
40
|
+
transition={{ delay, duration: 0.3 }}
|
|
41
|
+
>
|
|
42
|
+
<svg width={size} height={size} viewBox="0 0 56 56" fill="none">
|
|
43
|
+
<motion.circle
|
|
44
|
+
cx="28" cy="28" r="26" stroke="#22E3E8" strokeWidth="1.5"
|
|
45
|
+
fill="none"
|
|
46
|
+
initial={{ pathLength: 0, opacity: 0 }}
|
|
47
|
+
animate={{ pathLength: 1, opacity: 0.6 }}
|
|
48
|
+
transition={{ duration: 0.7, delay: delay + 0.1, ease: "easeOut" }}
|
|
49
|
+
/>
|
|
50
|
+
<motion.path
|
|
51
|
+
d="M17 28l8 8 14-14" stroke="#22E3E8" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
|
|
52
|
+
fill="none"
|
|
53
|
+
initial={{ pathLength: 0 }}
|
|
54
|
+
animate={{ pathLength: 1 }}
|
|
55
|
+
transition={{ duration: 0.4, delay: delay + 0.55, ease: "easeOut" }}
|
|
56
|
+
/>
|
|
57
|
+
</svg>
|
|
58
|
+
</motion.div>
|
|
59
|
+
</motion.div>
|
|
50
60
|
);
|
|
51
61
|
|
|
52
62
|
// ─── Props ───────────────────────────────────────────────────────────────────
|
|
@@ -68,7 +78,6 @@ export const BetCelebration = ({
|
|
|
68
78
|
}: BetCelebrationProps) => {
|
|
69
79
|
const { totalEntry, compoundedReward } = betSummary;
|
|
70
80
|
|
|
71
|
-
// Build list of bets to display
|
|
72
81
|
const displayBets = useMemo(() => {
|
|
73
82
|
const entries: { mIdx: number; question: string; optionLabel: string; amount: number; odds: number; reward: number }[] = [];
|
|
74
83
|
if (isEdit && editedMarketIdx !== undefined) {
|
|
@@ -93,84 +102,81 @@ export const BetCelebration = ({
|
|
|
93
102
|
}, [bets, config, isEdit, editedMarketIdx]);
|
|
94
103
|
|
|
95
104
|
// Timing
|
|
96
|
-
const particleCount = isEdit ?
|
|
97
|
-
const checkDelay = 0.
|
|
98
|
-
const titleDelay = checkDelay + 0.
|
|
99
|
-
const betStartDelay = isEdit ? 0.5 : 0.
|
|
100
|
-
const betStagger = isEdit ? 0.
|
|
101
|
-
const outcomeDelay = betStartDelay + displayBets.length * betStagger + 0.
|
|
102
|
-
const lineDelay =
|
|
103
|
-
const messageDelay = lineDelay + 0.
|
|
104
|
-
const buttonDelay = messageDelay + 0.
|
|
105
|
+
const particleCount = isEdit ? 8 : 24;
|
|
106
|
+
const checkDelay = 0.1;
|
|
107
|
+
const titleDelay = checkDelay + 0.5;
|
|
108
|
+
const betStartDelay = isEdit ? 0.5 : 0.9;
|
|
109
|
+
const betStagger = isEdit ? 0.08 : 0.12;
|
|
110
|
+
const outcomeDelay = betStartDelay + displayBets.length * betStagger + 0.3;
|
|
111
|
+
const lineDelay = isEdit ? betStartDelay + displayBets.length * betStagger + 0.2 : outcomeDelay + 0.4;
|
|
112
|
+
const messageDelay = lineDelay + 0.6;
|
|
113
|
+
const buttonDelay = messageDelay + 0.15;
|
|
105
114
|
|
|
106
115
|
return (
|
|
107
116
|
<motion.div
|
|
108
|
-
className="fixed inset-0 z-50 flex items-center justify-center px-
|
|
117
|
+
className="fixed inset-0 z-50 flex items-center justify-center px-6"
|
|
109
118
|
initial={{ opacity: 0 }}
|
|
110
119
|
animate={{ opacity: 1 }}
|
|
111
120
|
exit={{ opacity: 0 }}
|
|
112
|
-
transition={{ duration: 0.
|
|
121
|
+
transition={{ duration: 0.3 }}
|
|
113
122
|
>
|
|
114
|
-
{/*
|
|
123
|
+
{/* Blurred backdrop */}
|
|
115
124
|
<motion.div
|
|
116
125
|
className="absolute inset-0"
|
|
117
|
-
style={{ background: "rgba(0,0,0,0.
|
|
126
|
+
style={{ backdropFilter: "blur(16px)", WebkitBackdropFilter: "blur(16px)", background: "rgba(0,0,0,0.55)" }}
|
|
118
127
|
onClick={onClose}
|
|
119
128
|
/>
|
|
120
129
|
|
|
121
130
|
{/* Firework particles */}
|
|
122
131
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
|
123
132
|
{Array.from({ length: particleCount }, (_, i) => {
|
|
124
|
-
const angle = (360 / particleCount) * i + (i % 3) *
|
|
125
|
-
const distance =
|
|
126
|
-
const size = 2 + (i % 3) *
|
|
127
|
-
const colors = ["#22E3E8", "#22E3E8", "#22E3E8", "#
|
|
128
|
-
return <Particle key={i} delay={checkDelay + 0.
|
|
133
|
+
const angle = (360 / particleCount) * i + (i % 3) * 5;
|
|
134
|
+
const distance = 50 + (i % 5) * 30;
|
|
135
|
+
const size = 2 + (i % 3) * 1.5;
|
|
136
|
+
const colors = ["#22E3E8", "#22E3E8", "#22E3E8", "#22E3E8", "rgba(255,255,255,0.8)"];
|
|
137
|
+
return <Particle key={i} delay={checkDelay + 0.4 + (i % 6) * 0.04} angle={angle} distance={distance} size={size} color={colors[i % colors.length]} />;
|
|
129
138
|
})}
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
return <Particle key={`b${i}`} delay={outcomeDelay + (i % 4) * 0.05} angle={angle} distance={distance} size={size} color="#22E3E8" />;
|
|
139
|
+
{!isEdit && Array.from({ length: 16 }, (_, i) => {
|
|
140
|
+
const angle = (360 / 16) * i + 11;
|
|
141
|
+
const distance = 90 + (i % 4) * 30;
|
|
142
|
+
const size = 1.5 + (i % 3);
|
|
143
|
+
return <Particle key={`b${i}`} delay={outcomeDelay + (i % 5) * 0.04} angle={angle} distance={distance} size={size} color="#22E3E8" />;
|
|
136
144
|
})}
|
|
137
145
|
</div>
|
|
138
146
|
|
|
139
|
-
{/*
|
|
147
|
+
{/* Floating content — no card, no border */}
|
|
140
148
|
<motion.div
|
|
141
|
-
className="relative z-10 w-full max-w-[
|
|
142
|
-
|
|
143
|
-
initial={{ scale: 0.85, opacity: 0, y: 20 }}
|
|
149
|
+
className="relative z-10 w-full max-w-[320px] max-h-[80vh] overflow-y-auto flex flex-col items-center gap-4 py-4"
|
|
150
|
+
initial={{ scale: 0.9, opacity: 0, y: 16 }}
|
|
144
151
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
145
|
-
transition={{ type: "spring", stiffness:
|
|
152
|
+
transition={{ type: "spring", stiffness: 250, damping: 20, delay: 0.05 }}
|
|
146
153
|
onClick={(e) => e.stopPropagation()}
|
|
147
154
|
>
|
|
148
155
|
{/* Checkmark */}
|
|
149
|
-
<AnimatedCheck delay={checkDelay} size={isEdit ?
|
|
156
|
+
<AnimatedCheck delay={checkDelay} size={isEdit ? 40 : 52} />
|
|
150
157
|
|
|
151
158
|
{/* Title */}
|
|
152
159
|
<motion.p
|
|
153
160
|
className="font-bold text-white text-center"
|
|
154
|
-
style={{ ...OUTFIT, fontSize: isEdit ?
|
|
155
|
-
initial={{ opacity: 0, y:
|
|
161
|
+
style={{ ...OUTFIT, fontSize: isEdit ? 15 : 18, letterSpacing: "0.02em" }}
|
|
162
|
+
initial={{ opacity: 0, y: 6 }}
|
|
156
163
|
animate={{ opacity: 1, y: 0 }}
|
|
157
164
|
transition={{ delay: titleDelay, duration: 0.3 }}
|
|
158
165
|
>
|
|
159
|
-
{isEdit ? "Bet Updated
|
|
166
|
+
{isEdit ? "Bet Updated" : "Bets Placed"}
|
|
160
167
|
</motion.p>
|
|
161
168
|
|
|
162
|
-
{/* Bet items —
|
|
163
|
-
<div className="w-full flex flex-col gap-
|
|
169
|
+
{/* Bet items — floating, no borders */}
|
|
170
|
+
<div className="w-full flex flex-col gap-2 mt-1">
|
|
164
171
|
{displayBets.map((bet, i) => (
|
|
165
172
|
<motion.div
|
|
166
173
|
key={bet.mIdx}
|
|
167
|
-
className="w-full px-3 py-2
|
|
168
|
-
|
|
169
|
-
initial={{ opacity: 0, x: -16 }}
|
|
174
|
+
className="w-full px-3 py-2"
|
|
175
|
+
initial={{ opacity: 0, x: -12 }}
|
|
170
176
|
animate={{ opacity: 1, x: 0 }}
|
|
171
177
|
transition={{ delay: betStartDelay + i * betStagger, duration: 0.3, ease: "easeOut" }}
|
|
172
178
|
>
|
|
173
|
-
<p className="text-[
|
|
179
|
+
<p className="text-[9px] text-white/30 font-medium truncate mb-1" style={OUTFIT}>{bet.question}</p>
|
|
174
180
|
<div className="flex items-center justify-between">
|
|
175
181
|
<div className="flex items-center gap-1.5">
|
|
176
182
|
<SelectedCheck size={8} />
|
|
@@ -179,9 +185,9 @@ export const BetCelebration = ({
|
|
|
179
185
|
<div className="flex items-center gap-[3px]">
|
|
180
186
|
<PointsIcon size={7} />
|
|
181
187
|
<span className="text-[9px] text-white font-semibold" style={OUTFIT}>{bet.amount}</span>
|
|
182
|
-
<span className="text-[8px] text-white/
|
|
183
|
-
<span className="text-[9px] text-white/
|
|
184
|
-
<span className="text-[8px] text-white/
|
|
188
|
+
<span className="text-[8px] text-white/30" style={OUTFIT}>×</span>
|
|
189
|
+
<span className="text-[9px] text-white/50 font-semibold" style={OUTFIT}>{bet.odds}</span>
|
|
190
|
+
<span className="text-[8px] text-white/30" style={OUTFIT}>=</span>
|
|
185
191
|
<PointsIcon size={7} />
|
|
186
192
|
<span className="text-[9px] text-[#22E3E8] font-bold" style={OUTFIT}>{bet.reward}</span>
|
|
187
193
|
</div>
|
|
@@ -193,67 +199,60 @@ export const BetCelebration = ({
|
|
|
193
199
|
{/* Max outcome */}
|
|
194
200
|
{!isEdit && totalEntry > 0 && (
|
|
195
201
|
<motion.div
|
|
196
|
-
className="
|
|
197
|
-
initial={{ opacity: 0, scale: 0.
|
|
202
|
+
className="flex items-center justify-center gap-2 pt-1"
|
|
203
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
198
204
|
animate={{ opacity: 1, scale: 1 }}
|
|
199
205
|
transition={{ delay: outcomeDelay, type: "spring", stiffness: 200, damping: 18 }}
|
|
200
206
|
>
|
|
201
|
-
<span className="text-[
|
|
207
|
+
<span className="text-[10px] text-white/40 font-medium" style={OUTFIT}>Max outcome</span>
|
|
202
208
|
<div className="flex items-center gap-1">
|
|
203
|
-
<Image src="/iamgame_square_logo.jpg" alt="" width={
|
|
204
|
-
<span className="text-[
|
|
209
|
+
<Image src="/iamgame_square_logo.jpg" alt="" width={12} height={12} className="rounded-[2px]" />
|
|
210
|
+
<span className="text-[16px] text-white font-bold" style={OUTFIT}>{totalEntry.toLocaleString()}</span>
|
|
205
211
|
</div>
|
|
206
|
-
<span className="text-[
|
|
212
|
+
<span className="text-[16px] text-white/20 font-bold" style={OUTFIT}>→</span>
|
|
207
213
|
<div className="flex items-center gap-1 relative">
|
|
208
|
-
<Image src="/iamgame_square_logo.jpg" alt="" width={
|
|
209
|
-
<span className="text-[
|
|
210
|
-
<motion.div className="absolute inset-0 pointer-events-none overflow-hidden rounded">
|
|
211
|
-
<motion.div
|
|
212
|
-
className="absolute inset-y-0 w-[50%]"
|
|
213
|
-
style={{ background: "linear-gradient(90deg, transparent, rgba(34,227,232,0.25), transparent)" }}
|
|
214
|
-
animate={{ left: ["-50%", "150%"] }}
|
|
215
|
-
transition={{ duration: 1.8, repeat: Infinity, repeatDelay: 2.5, ease: "easeInOut", delay: outcomeDelay + 0.3 }}
|
|
216
|
-
/>
|
|
217
|
-
</motion.div>
|
|
214
|
+
<Image src="/iamgame_square_logo.jpg" alt="" width={12} height={12} className="rounded-[2px]" />
|
|
215
|
+
<span className="text-[16px] text-[#22E3E8] font-bold" style={{ ...OUTFIT, textShadow: "0 0 12px rgba(34,227,232,0.4)" }}>{compoundedReward.toLocaleString()}</span>
|
|
218
216
|
</div>
|
|
219
217
|
</motion.div>
|
|
220
218
|
)}
|
|
221
219
|
|
|
222
220
|
{/* Line growth */}
|
|
223
221
|
<motion.div
|
|
224
|
-
className="w-
|
|
225
|
-
style={{ background: "rgba(
|
|
222
|
+
className="w-3/4 h-px rounded-full overflow-hidden"
|
|
223
|
+
style={{ background: "rgba(255,255,255,0.06)" }}
|
|
226
224
|
initial={{ opacity: 0 }}
|
|
227
225
|
animate={{ opacity: 1 }}
|
|
228
226
|
transition={{ delay: lineDelay }}
|
|
229
227
|
>
|
|
230
228
|
<motion.div
|
|
231
229
|
className="h-full rounded-full"
|
|
232
|
-
style={{ background: "linear-gradient(90deg, #22E3E8,
|
|
230
|
+
style={{ background: "linear-gradient(90deg, transparent, #22E3E8, transparent)" }}
|
|
233
231
|
initial={{ width: "0%" }}
|
|
234
232
|
animate={{ width: "100%" }}
|
|
235
|
-
transition={{ delay: lineDelay + 0.1, duration: 0.
|
|
233
|
+
transition={{ delay: lineDelay + 0.1, duration: 0.8, ease: "easeOut" }}
|
|
236
234
|
/>
|
|
237
235
|
</motion.div>
|
|
238
236
|
|
|
239
237
|
{/* Message */}
|
|
240
238
|
<motion.p
|
|
241
|
-
className="text-[11px] text-white/
|
|
239
|
+
className="text-[11px] text-white/35 text-center font-medium leading-relaxed"
|
|
242
240
|
style={OUTFIT}
|
|
243
241
|
initial={{ opacity: 0 }}
|
|
244
242
|
animate={{ opacity: 1 }}
|
|
245
243
|
transition={{ delay: messageDelay, duration: 0.4 }}
|
|
246
244
|
>
|
|
247
|
-
Track the leaderboard to see your position
|
|
245
|
+
Track the leaderboard to see your position{"\n"}updating as bets resolve with the game
|
|
248
246
|
</motion.p>
|
|
249
247
|
|
|
250
248
|
{/* Leaderboard button */}
|
|
251
249
|
{onViewLeaderboard && (
|
|
252
250
|
<motion.button
|
|
253
251
|
onClick={onViewLeaderboard}
|
|
254
|
-
className="
|
|
255
|
-
style={{ ...OUTFIT, background: "
|
|
256
|
-
|
|
252
|
+
className="px-8 py-2 rounded-full text-[12px] font-semibold"
|
|
253
|
+
style={{ ...OUTFIT, background: "rgba(34,227,232,0.12)", color: "#22E3E8", border: "1px solid rgba(34,227,232,0.25)" }}
|
|
254
|
+
whileTap={{ scale: 0.96 }}
|
|
255
|
+
initial={{ opacity: 0, y: 6 }}
|
|
257
256
|
animate={{ opacity: 1, y: 0 }}
|
|
258
257
|
transition={{ delay: buttonDelay, type: "spring", stiffness: 200, damping: 18 }}
|
|
259
258
|
>
|
|
@@ -1,149 +1,272 @@
|
|
|
1
1
|
// @devrongx/games — games/prematch-bets/FullLeaderboard.tsx
|
|
2
|
-
// Full paginated leaderboard
|
|
2
|
+
// Full paginated leaderboard — immersive dark design matching game screen.
|
|
3
3
|
"use client";
|
|
4
4
|
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import {
|
|
5
|
+
import { useState, useMemo } from "react";
|
|
6
|
+
import { motion } from "framer-motion";
|
|
7
|
+
import { ArrowLeft, Loader2, Trophy, TrendingUp, Wallet } from "lucide-react";
|
|
7
8
|
import { IChallengeConfig } from "./config";
|
|
8
|
-
import {
|
|
9
|
-
import { OUTFIT } from "./constants";
|
|
9
|
+
import { OUTFIT, PointsIcon } from "./constants";
|
|
10
10
|
import { useTDLeaderboard } from "../../pools/hooks";
|
|
11
|
+
import type { ITDLeaderboardEntry } from "../../pools/types";
|
|
11
12
|
|
|
12
13
|
const PAGE_SIZE = 50;
|
|
13
14
|
|
|
14
15
|
const SORT_TABS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ key: "questions_left", label: "Left" },
|
|
16
|
+
{ key: "max_possible", label: "Score", icon: Trophy, desc: "Max potential outcome" },
|
|
17
|
+
{ key: "total_risked", label: "Risk", icon: TrendingUp, desc: "Total coins wagered" },
|
|
18
|
+
{ key: "current_coins", label: "Balance", icon: Wallet, desc: "Remaining coin balance" },
|
|
19
19
|
] as const;
|
|
20
20
|
|
|
21
|
+
const RANK_COLORS = [
|
|
22
|
+
{ bg: "rgba(255,215,0,0.12)", border: "rgba(255,215,0,0.3)", text: "#FFD700", glow: "0 0 12px rgba(255,215,0,0.2)" },
|
|
23
|
+
{ bg: "rgba(192,192,192,0.10)", border: "rgba(192,192,192,0.25)", text: "#C0C0C0", glow: "0 0 10px rgba(192,192,192,0.15)" },
|
|
24
|
+
{ bg: "rgba(205,127,50,0.10)", border: "rgba(205,127,50,0.25)", text: "#CD7F32", glow: "0 0 10px rgba(205,127,50,0.15)" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function getMetric(entry: ITDLeaderboardEntry, sortKey: string): number {
|
|
28
|
+
switch (sortKey) {
|
|
29
|
+
case "total_risked": return entry.total_risked;
|
|
30
|
+
case "current_coins": return entry.current_coins;
|
|
31
|
+
default: return entry.max_possible;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
interface FullLeaderboardProps {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
poolId: number;
|
|
37
|
+
config: IChallengeConfig;
|
|
38
|
+
onBack: () => void;
|
|
39
|
+
userEntryId?: number | null;
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
export function FullLeaderboard({ poolId, config, onBack }: FullLeaderboardProps) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
background: sortBy === tab.key ? "rgba(34,227,232,0.15)" : "rgba(255,255,255,0.04)",
|
|
61
|
-
color: sortBy === tab.key ? "#22E3E8" : "rgba(255,255,255,0.4)",
|
|
62
|
-
border: sortBy === tab.key ? "1px solid rgba(34,227,232,0.3)" : "1px solid transparent",
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
{tab.label}
|
|
66
|
-
</button>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
{/* Column headers */}
|
|
71
|
-
<div className="flex items-center gap-2 px-4 py-1.5" style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
|
|
72
|
-
<span className="w-[28px] text-right text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>#</span>
|
|
73
|
-
<span className="flex-1 text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>Player</span>
|
|
74
|
-
<span className="w-[52px] text-right text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>Score</span>
|
|
75
|
-
<span className="w-[52px] text-right text-[8px] text-white/25 uppercase tracking-wide" style={OUTFIT}>Payout</span>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
{/* Rows */}
|
|
79
|
-
{loading ? (
|
|
80
|
-
<div className="flex items-center justify-center py-10">
|
|
81
|
-
<Loader2 size={18} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} />
|
|
82
|
-
</div>
|
|
83
|
-
) : (
|
|
84
|
-
<div className="flex flex-col overflow-y-auto" style={{ maxHeight: "55vh" }}>
|
|
85
|
-
{rankings.map((r) => (
|
|
86
|
-
<LeaderboardRow
|
|
87
|
-
key={r.entry_id}
|
|
88
|
-
entry={{
|
|
89
|
-
wallet: r.partner_ext_id ?? `User #${r.user_id}`,
|
|
90
|
-
pts: r.current_coins,
|
|
91
|
-
payout: 0,
|
|
92
|
-
isYou: false,
|
|
93
|
-
rank: r.rank,
|
|
94
|
-
}}
|
|
95
|
-
rank={r.rank}
|
|
96
|
-
isLast={false}
|
|
97
|
-
/>
|
|
98
|
-
))}
|
|
99
|
-
{!loading && rankings.length === 0 && (
|
|
100
|
-
<div className="flex items-center justify-center py-10">
|
|
101
|
-
<p className="text-[11px] text-white/30" style={OUTFIT}>No entries yet</p>
|
|
42
|
+
export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLeaderboardProps) {
|
|
43
|
+
const [sortBy, setSortBy] = useState<string>("max_possible");
|
|
44
|
+
const [page, setPage] = useState(0);
|
|
45
|
+
|
|
46
|
+
const { rankings, total, loading } = useTDLeaderboard(poolId, {
|
|
47
|
+
sort: sortBy,
|
|
48
|
+
limit: PAGE_SIZE,
|
|
49
|
+
offset: page * PAGE_SIZE,
|
|
50
|
+
pollMs: 15_000,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const maxMetric = useMemo(() => {
|
|
54
|
+
if (rankings.length === 0) return 1;
|
|
55
|
+
return Math.max(...rankings.map((r) => getMetric(r, sortBy)), 1);
|
|
56
|
+
}, [rankings, sortBy]);
|
|
57
|
+
|
|
58
|
+
const activeTab = SORT_TABS.find((t) => t.key === sortBy) ?? SORT_TABS[0];
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="w-full flex flex-col">
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<div className="flex items-center gap-3 px-4 pt-4 pb-3">
|
|
64
|
+
<button onClick={onBack}
|
|
65
|
+
className="flex items-center justify-center w-8 h-8 rounded-xl"
|
|
66
|
+
style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
|
67
|
+
<ArrowLeft size={14} className="text-white/60" />
|
|
68
|
+
</button>
|
|
69
|
+
<div className="flex-1">
|
|
70
|
+
<p className="text-[15px] font-bold text-white tracking-wide" style={OUTFIT}>Leaderboard</p>
|
|
71
|
+
<p className="text-[10px] text-white/35 font-medium" style={OUTFIT}>
|
|
72
|
+
{total.toLocaleString()} player{total !== 1 ? "s" : ""} · {activeTab.desc}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
102
75
|
</div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
className="
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
76
|
+
|
|
77
|
+
{/* Sort tabs — pill style matching game FilterPill */}
|
|
78
|
+
<div className="flex gap-1.5 px-4 py-2">
|
|
79
|
+
{SORT_TABS.map((tab) => {
|
|
80
|
+
const active = sortBy === tab.key;
|
|
81
|
+
const TabIcon = tab.icon;
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
key={tab.key}
|
|
85
|
+
onClick={() => { setSortBy(tab.key); setPage(0); }}
|
|
86
|
+
className="flex-1 flex items-center justify-center gap-1 py-1.5 rounded-full text-[10px] font-semibold transition-all"
|
|
87
|
+
style={{
|
|
88
|
+
...OUTFIT,
|
|
89
|
+
border: `1px solid ${active ? "#22E3E8" : "rgba(34,227,232,0.15)"}`,
|
|
90
|
+
background: active ? "rgba(34,227,232,0.12)" : "transparent",
|
|
91
|
+
color: active ? "#22E3E8" : "rgba(255,255,255,0.4)",
|
|
92
|
+
boxShadow: active ? "0 0 12px rgba(34,227,232,0.1)" : "none",
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<TabIcon size={10} />
|
|
96
|
+
{tab.label}
|
|
97
|
+
</button>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Column guide */}
|
|
103
|
+
<div className="flex items-center px-4 py-1.5 mt-1" style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
|
|
104
|
+
<span className="w-[32px] text-[7px] text-white/20 uppercase tracking-widest font-bold text-center" style={OUTFIT}>Rank</span>
|
|
105
|
+
<span className="flex-1 text-[7px] text-white/20 uppercase tracking-widest font-bold pl-2" style={OUTFIT}>Player</span>
|
|
106
|
+
<span className="w-[80px] text-[7px] text-white/20 uppercase tracking-widest font-bold text-right" style={OUTFIT}>{activeTab.label}</span>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Rows */}
|
|
110
|
+
{loading ? (
|
|
111
|
+
<div className="flex items-center justify-center py-16">
|
|
112
|
+
<Loader2 size={18} className="animate-spin" style={{ color: "rgba(34,227,232,0.4)" }} />
|
|
113
|
+
</div>
|
|
114
|
+
) : (
|
|
115
|
+
<div className="flex flex-col overflow-y-auto" style={{ maxHeight: "55vh" }}>
|
|
116
|
+
{rankings.map((r, i) => {
|
|
117
|
+
const isUser = r.entry_id === userEntryId;
|
|
118
|
+
const rankIdx = r.rank - 1;
|
|
119
|
+
const isTop3 = rankIdx < 3;
|
|
120
|
+
const metric = getMetric(r, sortBy);
|
|
121
|
+
const barWidth = maxMetric > 0 ? (metric / maxMetric) * 100 : 0;
|
|
122
|
+
const rankStyle = isTop3 ? RANK_COLORS[rankIdx] : null;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<motion.div
|
|
126
|
+
key={r.entry_id}
|
|
127
|
+
initial={{ opacity: 0, x: -8 }}
|
|
128
|
+
animate={{ opacity: 1, x: 0 }}
|
|
129
|
+
transition={{ delay: i * 0.03, duration: 0.25 }}
|
|
130
|
+
className="relative px-4 py-2"
|
|
131
|
+
style={{
|
|
132
|
+
background: isUser ? "rgba(34,227,232,0.06)" : "transparent",
|
|
133
|
+
borderBottom: "1px solid rgba(255,255,255,0.03)",
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{/* Cyan left accent for user */}
|
|
137
|
+
{isUser && (
|
|
138
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-[60%] rounded-full"
|
|
139
|
+
style={{ background: "#22E3E8", boxShadow: "0 0 8px rgba(34,227,232,0.4)" }} />
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<div className="flex items-center">
|
|
143
|
+
{/* Rank badge */}
|
|
144
|
+
<div className="w-[32px] flex-shrink-0 flex items-center justify-center">
|
|
145
|
+
{isTop3 && rankStyle ? (
|
|
146
|
+
<span
|
|
147
|
+
className="inline-flex items-center justify-center w-[22px] h-[22px] rounded-lg text-[10px] font-bold"
|
|
148
|
+
style={{ background: rankStyle.bg, border: `1px solid ${rankStyle.border}`, color: rankStyle.text, boxShadow: rankStyle.glow, ...OUTFIT }}>
|
|
149
|
+
{r.rank}
|
|
150
|
+
</span>
|
|
151
|
+
) : (
|
|
152
|
+
<span className="text-[11px] text-white/40 font-semibold" style={OUTFIT}>{r.rank}</span>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Player */}
|
|
157
|
+
<div className="flex-1 flex items-center gap-1.5 pl-2 min-w-0">
|
|
158
|
+
<span className={`text-[11px] font-semibold truncate ${isUser ? "text-[#22E3E8]" : isTop3 ? "text-white" : "text-white/70"}`} style={OUTFIT}>
|
|
159
|
+
{r.partner_ext_id ?? `User #${r.user_id}`}
|
|
160
|
+
</span>
|
|
161
|
+
{isUser && (
|
|
162
|
+
<span className="text-[7px] px-1.5 py-[1px] rounded-full font-bold uppercase flex-shrink-0"
|
|
163
|
+
style={{ background: "rgba(34,227,232,0.15)", color: "#22E3E8", border: "1px solid rgba(34,227,232,0.2)", ...OUTFIT }}>
|
|
164
|
+
You
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Primary metric */}
|
|
170
|
+
<div className="w-[80px] flex-shrink-0 flex items-center justify-end gap-1">
|
|
171
|
+
<PointsIcon size={9} />
|
|
172
|
+
<span
|
|
173
|
+
className={`text-[12px] font-bold ${isUser ? "text-[#22E3E8]" : isTop3 ? "text-white" : "text-white/80"}`}
|
|
174
|
+
style={{ ...OUTFIT, ...(isUser ? { textShadow: "0 0 8px rgba(34,227,232,0.3)" } : {}) }}>
|
|
175
|
+
{metric.toLocaleString()}
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{/* Secondary info + progress bar */}
|
|
181
|
+
<div className="flex items-center mt-1 pl-[34px]">
|
|
182
|
+
<div className="flex-1 flex items-center gap-2">
|
|
183
|
+
<span className="text-[8px] text-white/25 font-medium" style={OUTFIT}>
|
|
184
|
+
{r.bets_placed} bet{r.bets_placed !== 1 ? "s" : ""}
|
|
185
|
+
</span>
|
|
186
|
+
{sortBy !== "total_risked" && r.total_risked > 0 && (
|
|
187
|
+
<span className="text-[8px] text-white/20 font-medium" style={OUTFIT}>
|
|
188
|
+
· {r.total_risked.toLocaleString()} risked
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
{sortBy !== "max_possible" && r.max_possible > 0 && (
|
|
192
|
+
<span className="text-[8px] text-white/20 font-medium" style={OUTFIT}>
|
|
193
|
+
· {r.max_possible.toLocaleString()} max
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Thin progress bar */}
|
|
200
|
+
<div className="mt-1 ml-[34px] h-[2px] rounded-full overflow-hidden" style={{ background: "rgba(255,255,255,0.04)" }}>
|
|
201
|
+
<motion.div
|
|
202
|
+
className="h-full rounded-full"
|
|
203
|
+
initial={{ width: 0 }}
|
|
204
|
+
animate={{ width: `${barWidth}%` }}
|
|
205
|
+
transition={{ delay: i * 0.03 + 0.15, duration: 0.5, ease: "easeOut" }}
|
|
206
|
+
style={{
|
|
207
|
+
background: isUser
|
|
208
|
+
? "linear-gradient(90deg, rgba(34,227,232,0.6), rgba(34,227,232,0.2))"
|
|
209
|
+
: isTop3 && rankStyle
|
|
210
|
+
? `linear-gradient(90deg, ${rankStyle.text}80, ${rankStyle.text}20)`
|
|
211
|
+
: "linear-gradient(90deg, rgba(255,255,255,0.2), rgba(255,255,255,0.05))",
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
</motion.div>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
{!loading && rankings.length === 0 && (
|
|
219
|
+
<div className="flex items-center justify-center py-16">
|
|
220
|
+
<p className="text-[11px] text-white/30 font-medium" style={OUTFIT}>No entries yet</p>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Pagination — only when needed */}
|
|
227
|
+
{total > PAGE_SIZE && (
|
|
228
|
+
<div className="flex items-center justify-center gap-3 px-4 py-3" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
|
229
|
+
<button
|
|
230
|
+
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
|
231
|
+
disabled={page === 0}
|
|
232
|
+
className="text-[10px] font-semibold px-4 py-1.5 rounded-full disabled:opacity-25 transition-all"
|
|
233
|
+
style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)", color: "rgba(255,255,255,0.6)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
|
234
|
+
Prev
|
|
235
|
+
</button>
|
|
236
|
+
<span className="text-[10px] text-white/35 font-medium" style={OUTFIT}>
|
|
237
|
+
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
|
|
238
|
+
</span>
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => setPage((p) => p + 1)}
|
|
241
|
+
disabled={(page + 1) * PAGE_SIZE >= total}
|
|
242
|
+
className="text-[10px] font-semibold px-4 py-1.5 rounded-full disabled:opacity-25 transition-all"
|
|
243
|
+
style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)", color: "rgba(255,255,255,0.6)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
|
244
|
+
Next
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{/* Prize brackets */}
|
|
250
|
+
{config.rankBrackets.length > 0 && (
|
|
251
|
+
<div className="px-4 pb-4 pt-2">
|
|
252
|
+
<p className="text-[8px] text-white/25 font-bold uppercase tracking-widest mb-2" style={OUTFIT}>Prize Breakdown</p>
|
|
253
|
+
<div className="flex flex-wrap gap-1.5">
|
|
254
|
+
{config.rankBrackets.map((b, i) => (
|
|
255
|
+
<span
|
|
256
|
+
key={i}
|
|
257
|
+
className="text-[8px] font-semibold px-2.5 py-1 rounded-full"
|
|
258
|
+
style={{
|
|
259
|
+
background: i === 0 ? "rgba(255,215,0,0.08)" : "rgba(255,255,255,0.04)",
|
|
260
|
+
color: i === 0 ? "#FFD700" : "rgba(255,255,255,0.35)",
|
|
261
|
+
border: `1px solid ${i === 0 ? "rgba(255,215,0,0.2)" : "rgba(255,255,255,0.06)"}`,
|
|
262
|
+
...OUTFIT,
|
|
263
|
+
}}>
|
|
264
|
+
#{b.from}{b.to > b.from ? `–${b.to}` : ""}: {(b.poolPercent * 100).toFixed(0)}%
|
|
265
|
+
</span>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
145
270
|
</div>
|
|
146
|
-
|
|
147
|
-
</div>
|
|
148
|
-
);
|
|
271
|
+
);
|
|
149
272
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// @devrongx/games — games/prematch-bets/LeaderboardRow.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import Image from "next/image";
|
|
5
4
|
import { ILeaderboardEntry } from "./config";
|
|
5
|
+
import { OUTFIT, PointsIcon } from "./constants";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{
|
|
7
|
+
const RANK_COLORS = [
|
|
8
|
+
{ bg: "rgba(255,215,0,0.12)", border: "rgba(255,215,0,0.3)", text: "#FFD700" },
|
|
9
|
+
{ bg: "rgba(192,192,192,0.10)", border: "rgba(192,192,192,0.25)", text: "#C0C0C0" },
|
|
10
|
+
{ bg: "rgba(205,127,50,0.10)", border: "rgba(205,127,50,0.25)", text: "#CD7F32" },
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
interface LeaderboardRowProps {
|
|
@@ -18,50 +18,53 @@ interface LeaderboardRowProps {
|
|
|
18
18
|
|
|
19
19
|
export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) => {
|
|
20
20
|
const rankIdx = rank - 1;
|
|
21
|
+
const isTop3 = rankIdx < 3;
|
|
22
|
+
const rankStyle = isTop3 ? RANK_COLORS[rankIdx] : null;
|
|
21
23
|
|
|
22
24
|
return (
|
|
23
25
|
<div
|
|
24
|
-
className=
|
|
26
|
+
className="relative flex items-center gap-2 px-3 py-[6px] transition-all duration-300"
|
|
25
27
|
style={{
|
|
26
28
|
borderBottom: isLast ? "none" : "1px solid rgba(255,255,255,0.03)",
|
|
27
|
-
background: entry.isYou ? "rgba(34,227,232,0.
|
|
29
|
+
background: entry.isYou ? "rgba(34,227,232,0.06)" : "transparent",
|
|
28
30
|
}}
|
|
29
31
|
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
{entry.isYou && (
|
|
33
|
+
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-[60%] rounded-full"
|
|
34
|
+
style={{ background: "#22E3E8", boxShadow: "0 0 6px rgba(34,227,232,0.3)" }} />
|
|
35
|
+
)}
|
|
36
|
+
<div className="w-[32px] flex-shrink-0 flex items-center justify-center">
|
|
37
|
+
{isTop3 && rankStyle ? (
|
|
32
38
|
<span
|
|
33
|
-
className="inline-flex items-center justify-center w-[
|
|
34
|
-
style={{ background:
|
|
35
|
-
>
|
|
39
|
+
className="inline-flex items-center justify-center w-[20px] h-[20px] rounded-lg text-[9px] font-bold"
|
|
40
|
+
style={{ background: rankStyle.bg, border: `1px solid ${rankStyle.border}`, color: rankStyle.text, ...OUTFIT }}>
|
|
36
41
|
{rank}
|
|
37
42
|
</span>
|
|
38
43
|
) : (
|
|
39
|
-
<span className="text-[10px] text-white/
|
|
44
|
+
<span className="text-[10px] text-white/40 font-semibold" style={OUTFIT}>{rank.toLocaleString()}</span>
|
|
40
45
|
)}
|
|
41
46
|
</div>
|
|
42
47
|
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
|
43
48
|
<span
|
|
44
|
-
className={`text-[11px] truncate ${entry.isYou ? "text-[#22E3E8]
|
|
45
|
-
style={
|
|
46
|
-
>
|
|
49
|
+
className={`text-[11px] truncate font-semibold ${entry.isYou ? "text-[#22E3E8]" : isTop3 ? "text-white" : "text-white/60"}`}
|
|
50
|
+
style={OUTFIT}>
|
|
47
51
|
{entry.wallet}
|
|
48
52
|
</span>
|
|
49
53
|
{entry.isYou && (
|
|
50
|
-
<span className="text-[7px] px-1 py-[1px] rounded
|
|
54
|
+
<span className="text-[7px] px-1.5 py-[1px] rounded-full font-bold uppercase flex-shrink-0"
|
|
55
|
+
style={{ background: "rgba(34,227,232,0.15)", color: "#22E3E8", border: "1px solid rgba(34,227,232,0.2)", ...OUTFIT }}>
|
|
56
|
+
You
|
|
57
|
+
</span>
|
|
51
58
|
)}
|
|
52
59
|
</div>
|
|
53
60
|
<div className="w-[66px] flex-shrink-0 flex items-center justify-end gap-[3px]">
|
|
54
|
-
<
|
|
55
|
-
<span
|
|
61
|
+
<PointsIcon size={9} />
|
|
62
|
+
<span
|
|
63
|
+
className={`text-[11px] font-bold ${entry.isYou ? "text-[#22E3E8]" : isTop3 ? "text-white" : "text-white/70"}`}
|
|
64
|
+
style={{ ...OUTFIT, ...(entry.isYou ? { textShadow: "0 0 6px rgba(34,227,232,0.3)" } : {}) }}>
|
|
56
65
|
{entry.pts.toLocaleString()}
|
|
57
66
|
</span>
|
|
58
67
|
</div>
|
|
59
|
-
<div className="w-[72px] flex-shrink-0 flex items-center justify-end gap-[2px]">
|
|
60
|
-
<span className={`text-[10px] font-semibold ${entry.isYou ? "text-[#22E3E8]" : "text-green-400"}`}>
|
|
61
|
-
+${entry.payout.toFixed(1)}
|
|
62
|
-
</span>
|
|
63
|
-
<Image src="/icons/ic_usdc.png" alt="" width={12} height={12} className="rounded-full" />
|
|
64
|
-
</div>
|
|
65
68
|
</div>
|
|
66
69
|
);
|
|
67
70
|
};
|
|
@@ -60,7 +60,7 @@ function buildMiniLeaderboard(
|
|
|
60
60
|
top.forEach((r) => {
|
|
61
61
|
rows.push({
|
|
62
62
|
wallet: r.partner_ext_id ?? `User #${r.user_id}`,
|
|
63
|
-
pts: r.
|
|
63
|
+
pts: r.max_possible,
|
|
64
64
|
payout: 0,
|
|
65
65
|
isYou: r.entry_id === userEntryId,
|
|
66
66
|
rank: r.rank,
|
|
@@ -103,7 +103,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
103
103
|
// ── Real API data (only when poolId provided) ────────────────────────────
|
|
104
104
|
const { pool, loading: poolLoading, refetch: refetchPool } = useTDPool(poolId ?? 0);
|
|
105
105
|
const { data: entryData, refetch: refetchEntry } = useTDPoolEntry(poolId ?? 0);
|
|
106
|
-
const { rankings, refetch: refetchLB } = useTDLeaderboard(poolId ?? 0, { pollMs: poolId ? 30_000 : 0 });
|
|
106
|
+
const { rankings, refetch: refetchLB } = useTDLeaderboard(poolId ?? 0, { sort: "max_possible", pollMs: poolId ? 30_000 : 0 });
|
|
107
107
|
|
|
108
108
|
// ── Config: real or fallback ─────────────────────────────────────────────
|
|
109
109
|
// matchProp is passed directly from MatchCalendar's onPoolPress — no extra fetch needed
|
|
@@ -429,6 +429,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
429
429
|
poolId={poolId ?? 0}
|
|
430
430
|
config={config}
|
|
431
431
|
onBack={() => setShowFullLeaderboard(false)}
|
|
432
|
+
userEntryId={entryData?.entry.id ?? null}
|
|
432
433
|
/>
|
|
433
434
|
</GamePopupShell>
|
|
434
435
|
);
|
|
@@ -840,12 +840,11 @@ export const PreMatchGame = ({
|
|
|
840
840
|
|
|
841
841
|
{/* Leaderboard & Potential Payouts */}
|
|
842
842
|
<div className="pt-1">
|
|
843
|
-
<p className="text-[10px] text-white uppercase tracking-wide mb-2 font-semibold" style={OUTFIT}>Leaderboard
|
|
843
|
+
<p className="text-[10px] text-white uppercase tracking-wide mb-2 font-semibold" style={OUTFIT}>Leaderboard</p>
|
|
844
844
|
<div className="flex items-center gap-2 px-3 mb-1">
|
|
845
|
-
<span className="w-[
|
|
846
|
-
<span className="flex-1 text-[
|
|
847
|
-
<span className="w-[66px] flex-shrink-0 text-right text-[
|
|
848
|
-
<span className="w-[72px] flex-shrink-0 text-right text-[9px] text-white/50 uppercase tracking-wide" style={OUTFIT}>Payout</span>
|
|
845
|
+
<span className="w-[32px] flex-shrink-0 text-center text-[8px] text-white/30 uppercase tracking-widest font-bold" style={OUTFIT}>#</span>
|
|
846
|
+
<span className="flex-1 text-[8px] text-white/30 uppercase tracking-widest font-bold pl-2" style={OUTFIT}>Player</span>
|
|
847
|
+
<span className="w-[66px] flex-shrink-0 text-right text-[8px] text-white/30 uppercase tracking-widest font-bold" style={OUTFIT}>Score</span>
|
|
849
848
|
</div>
|
|
850
849
|
<div className="h-px bg-white/5 mb-1" />
|
|
851
850
|
<div className="flex flex-col">
|