@devrongx/games 0.4.34 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -1,149 +1,272 @@
1
1
  // @devrongx/games — games/prematch-bets/FullLeaderboard.tsx
2
- // Full paginated leaderboard with sort tabs and pinned user rank.
2
+ // Full paginated leaderboard immersive dark design matching game screen.
3
3
  "use client";
4
4
 
5
- import { useState } from "react";
6
- import { ArrowLeft, Loader2 } from "lucide-react";
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 { LeaderboardRow } from "./LeaderboardRow";
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
- { key: "current_coins", label: "Score" },
16
- { key: "total_risked", label: "Risk" },
17
- { key: "max_possible", label: "Max" },
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
- poolId: number;
23
- config: IChallengeConfig;
24
- onBack: () => void;
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
- const [sortBy, setSortBy] = useState<string>("current_coins");
29
- const [page, setPage] = useState(0);
30
-
31
- const { rankings, total, loading } = useTDLeaderboard(poolId, {
32
- sort: sortBy,
33
- limit: PAGE_SIZE,
34
- offset: page * PAGE_SIZE,
35
- pollMs: 15_000,
36
- });
37
-
38
- return (
39
- <div className="w-full flex flex-col">
40
- {/* Header */}
41
- <div className="flex items-center gap-3 px-4 pt-4 pb-3" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
42
- <button onClick={onBack} className="flex items-center justify-center w-7 h-7 rounded-lg" style={{ background: "rgba(255,255,255,0.06)" }}>
43
- <ArrowLeft size={14} className="text-white/60" />
44
- </button>
45
- <div className="flex-1">
46
- <p className="text-[14px] font-bold text-white" style={OUTFIT}>Leaderboard</p>
47
- <p className="text-[10px] text-white/40" style={OUTFIT}>{total.toLocaleString()} players</p>
48
- </div>
49
- </div>
50
-
51
- {/* Sort tabs */}
52
- <div className="flex gap-1 px-4 py-2" style={{ borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
53
- {SORT_TABS.map((tab) => (
54
- <button
55
- key={tab.key}
56
- onClick={() => { setSortBy(tab.key); setPage(0); }}
57
- className="flex-1 py-1.5 rounded-lg text-[10px] font-semibold transition-all"
58
- style={{
59
- ...OUTFIT,
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
- </div>
105
- )}
106
-
107
- {/* Pagination */}
108
- <div className="flex items-center justify-center gap-3 px-4 py-3" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
109
- <button
110
- onClick={() => setPage((p) => Math.max(0, p - 1))}
111
- disabled={page === 0}
112
- className="text-[10px] font-semibold px-3 py-1.5 rounded-lg disabled:opacity-30"
113
- style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)", color: "rgba(255,255,255,0.6)" }}
114
- >
115
- ← Prev
116
- </button>
117
- <span className="text-[10px] text-white/40" style={OUTFIT}>
118
- {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}
119
- </span>
120
- <button
121
- onClick={() => setPage((p) => p + 1)}
122
- disabled={(page + 1) * PAGE_SIZE >= total}
123
- className="text-[10px] font-semibold px-3 py-1.5 rounded-lg disabled:opacity-30"
124
- style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)", color: "rgba(255,255,255,0.6)" }}
125
- >
126
- Next →
127
- </button>
128
- </div>
129
-
130
- {/* Payout brackets */}
131
- {config.rankBrackets.length > 0 && (
132
- <div className="px-4 pb-4">
133
- <p className="text-[9px] text-white/30 font-semibold uppercase tracking-wider mb-1.5" style={OUTFIT}>Payout Brackets</p>
134
- <div className="flex flex-wrap gap-1">
135
- {config.rankBrackets.map((b, i) => (
136
- <span
137
- key={i}
138
- className="text-[8px] font-medium px-2 py-0.5 rounded-full"
139
- style={{ background: "rgba(255,255,255,0.04)", color: "rgba(255,255,255,0.4)", border: "1px solid rgba(255,255,255,0.08)" }}
140
- >
141
- #{b.from}{b.to > b.from ? `–${b.to}` : ""}: {(b.poolPercent * 100).toFixed(0)}%
142
- </span>
143
- ))}
144
- </div>
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 RANK_STYLES = [
8
- { accent: "#FFD700" },
9
- { accent: "#C0C0C0" },
10
- { accent: "#CD7F32" },
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={`flex items-center gap-2 px-3 py-[5px] transition-all duration-700 ${entry.isYou ? "rounded" : ""}`}
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.08)" : "transparent",
29
+ background: entry.isYou ? "rgba(34,227,232,0.06)" : "transparent",
28
30
  }}
29
31
  >
30
- <div className="w-[46px] flex-shrink-0 text-right pr-1">
31
- {rankIdx < 3 ? (
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-[18px] h-[18px] rounded text-[9px] font-bold"
34
- style={{ background: `${RANK_STYLES[rankIdx].accent}15`, color: RANK_STYLES[rankIdx].accent, fontFamily: "Outfit, sans-serif" }}
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/50 font-semibold" style={{ fontFamily: "Outfit, sans-serif" }}>{rank.toLocaleString()}</span>
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] font-bold" : "text-white"}`}
45
- style={{ fontFamily: "Outfit, sans-serif" }}
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 bg-[#22E3E8]/20 text-[#22E3E8] font-bold uppercase flex-shrink-0">You</span>
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
- <Image src="/iamgame_square_logo.jpg" alt="" width={9} height={9} className="rounded-[2px]" />
55
- <span className={`text-[10px] font-semibold ${entry.isYou ? "text-[#22E3E8]" : "text-white"}`}>
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.current_coins,
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 & Potential Payouts</p>
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-[46px] flex-shrink-0 text-right pr-1 text-[9px] text-white/50 uppercase tracking-wide" style={OUTFIT}>#</span>
846
- <span className="flex-1 text-[9px] text-white/50 uppercase tracking-wide" style={OUTFIT}>Player</span>
847
- <span className="w-[66px] flex-shrink-0 text-right text-[9px] text-white/50 uppercase tracking-wide" style={OUTFIT}>Points</span>
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">