@devrongx/games 0.4.35 → 0.4.36
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
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"use client";
|
|
4
4
|
|
|
5
5
|
import { useState, useMemo } from "react";
|
|
6
|
-
import { motion } from "framer-motion";
|
|
7
|
-
import { ArrowLeft, Loader2, Trophy, TrendingUp
|
|
6
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
7
|
+
import { ArrowLeft, ChevronDown, ChevronUp, Loader2, Trophy, TrendingUp } from "lucide-react";
|
|
8
8
|
import { IChallengeConfig } from "./config";
|
|
9
9
|
import { OUTFIT, PointsIcon } from "./constants";
|
|
10
10
|
import { useTDLeaderboard } from "../../pools/hooks";
|
|
@@ -13,22 +13,27 @@ import type { ITDLeaderboardEntry } from "../../pools/types";
|
|
|
13
13
|
const PAGE_SIZE = 50;
|
|
14
14
|
|
|
15
15
|
const SORT_TABS = [
|
|
16
|
-
{ key: "max_possible", label: "Score", icon: Trophy
|
|
17
|
-
{ key: "
|
|
18
|
-
{ key: "current_coins", label: "Balance", icon: Wallet, desc: "Remaining coin balance" },
|
|
16
|
+
{ key: "max_possible", label: "Score", icon: Trophy },
|
|
17
|
+
{ key: "pending_risk", label: "Risk", icon: TrendingUp },
|
|
19
18
|
] as const;
|
|
20
19
|
|
|
20
|
+
const EXPLAINER: Record<string, string> = {
|
|
21
|
+
max_possible:
|
|
22
|
+
"Each unresolved bet counts at its max potential payout. As results come in, won bets keep their value, lost bets drop to zero. Your score converges to your actual outcome as the match progresses.",
|
|
23
|
+
pending_risk:
|
|
24
|
+
"Total coins at stake in unresolved bets. As each question is answered, that bet\u2019s risk drops to zero \u2014 win or lose. Risk reaches zero when all bets settle.",
|
|
25
|
+
};
|
|
26
|
+
|
|
21
27
|
const RANK_COLORS = [
|
|
22
28
|
{ 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
29
|
{ 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
30
|
{ 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
31
|
];
|
|
26
32
|
|
|
27
|
-
function getMetric(entry: ITDLeaderboardEntry, sortKey: string): number {
|
|
33
|
+
function getMetric(entry: ITDLeaderboardEntry, sortKey: string, startingBalance: number): number {
|
|
28
34
|
switch (sortKey) {
|
|
29
|
-
case "
|
|
30
|
-
|
|
31
|
-
default: return entry.max_possible;
|
|
35
|
+
case "pending_risk": return entry.pending_risk;
|
|
36
|
+
default: return entry.max_possible - startingBalance;
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -42,6 +47,7 @@ interface FullLeaderboardProps {
|
|
|
42
47
|
export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLeaderboardProps) {
|
|
43
48
|
const [sortBy, setSortBy] = useState<string>("max_possible");
|
|
44
49
|
const [page, setPage] = useState(0);
|
|
50
|
+
const [showExplainer, setShowExplainer] = useState(false);
|
|
45
51
|
|
|
46
52
|
const { rankings, total, loading } = useTDLeaderboard(poolId, {
|
|
47
53
|
sort: sortBy,
|
|
@@ -50,10 +56,12 @@ export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLea
|
|
|
50
56
|
pollMs: 15_000,
|
|
51
57
|
});
|
|
52
58
|
|
|
59
|
+
const startingBalance = config.startingBalance;
|
|
60
|
+
|
|
53
61
|
const maxMetric = useMemo(() => {
|
|
54
62
|
if (rankings.length === 0) return 1;
|
|
55
|
-
return Math.max(...rankings.map((r) => getMetric(r, sortBy)), 1);
|
|
56
|
-
}, [rankings, sortBy]);
|
|
63
|
+
return Math.max(...rankings.map((r) => getMetric(r, sortBy, startingBalance)), 1);
|
|
64
|
+
}, [rankings, sortBy, startingBalance]);
|
|
57
65
|
|
|
58
66
|
const activeTab = SORT_TABS.find((t) => t.key === sortBy) ?? SORT_TABS[0];
|
|
59
67
|
|
|
@@ -69,12 +77,12 @@ export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLea
|
|
|
69
77
|
<div className="flex-1">
|
|
70
78
|
<p className="text-[15px] font-bold text-white tracking-wide" style={OUTFIT}>Leaderboard</p>
|
|
71
79
|
<p className="text-[10px] text-white/35 font-medium" style={OUTFIT}>
|
|
72
|
-
{total.toLocaleString()} player{total !== 1 ? "s" : ""}
|
|
80
|
+
{total.toLocaleString()} player{total !== 1 ? "s" : ""}
|
|
73
81
|
</p>
|
|
74
82
|
</div>
|
|
75
83
|
</div>
|
|
76
84
|
|
|
77
|
-
{/* Sort tabs
|
|
85
|
+
{/* Sort tabs */}
|
|
78
86
|
<div className="flex gap-1.5 px-4 py-2">
|
|
79
87
|
{SORT_TABS.map((tab) => {
|
|
80
88
|
const active = sortBy === tab.key;
|
|
@@ -99,6 +107,32 @@ export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLea
|
|
|
99
107
|
})}
|
|
100
108
|
</div>
|
|
101
109
|
|
|
110
|
+
{/* Explainer toggle */}
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => setShowExplainer((v) => !v)}
|
|
113
|
+
className="flex items-center gap-1 px-4 py-1 text-[9px] font-medium"
|
|
114
|
+
style={{ ...OUTFIT, color: "rgba(34,227,232,0.5)" }}
|
|
115
|
+
>
|
|
116
|
+
How does this work?
|
|
117
|
+
{showExplainer ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
|
|
118
|
+
</button>
|
|
119
|
+
<AnimatePresence>
|
|
120
|
+
{showExplainer && (
|
|
121
|
+
<motion.div
|
|
122
|
+
initial={{ height: 0, opacity: 0 }}
|
|
123
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
124
|
+
exit={{ height: 0, opacity: 0 }}
|
|
125
|
+
transition={{ duration: 0.2 }}
|
|
126
|
+
className="overflow-hidden"
|
|
127
|
+
>
|
|
128
|
+
<p className="text-[9px] leading-relaxed px-4 pb-2 font-medium"
|
|
129
|
+
style={{ ...OUTFIT, color: "rgba(255,255,255,0.3)" }}>
|
|
130
|
+
{EXPLAINER[sortBy]}
|
|
131
|
+
</p>
|
|
132
|
+
</motion.div>
|
|
133
|
+
)}
|
|
134
|
+
</AnimatePresence>
|
|
135
|
+
|
|
102
136
|
{/* Column guide */}
|
|
103
137
|
<div className="flex items-center px-4 py-1.5 mt-1" style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
|
|
104
138
|
<span className="w-[32px] text-[7px] text-white/20 uppercase tracking-widest font-bold text-center" style={OUTFIT}>Rank</span>
|
|
@@ -117,7 +151,7 @@ export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLea
|
|
|
117
151
|
const isUser = r.entry_id === userEntryId;
|
|
118
152
|
const rankIdx = r.rank - 1;
|
|
119
153
|
const isTop3 = rankIdx < 3;
|
|
120
|
-
const metric = getMetric(r, sortBy);
|
|
154
|
+
const metric = getMetric(r, sortBy, startingBalance);
|
|
121
155
|
const barWidth = maxMetric > 0 ? (metric / maxMetric) * 100 : 0;
|
|
122
156
|
const rankStyle = isTop3 ? RANK_COLORS[rankIdx] : null;
|
|
123
157
|
|
|
@@ -183,14 +217,14 @@ export function FullLeaderboard({ poolId, config, onBack, userEntryId }: FullLea
|
|
|
183
217
|
<span className="text-[8px] text-white/25 font-medium" style={OUTFIT}>
|
|
184
218
|
{r.bets_placed} bet{r.bets_placed !== 1 ? "s" : ""}
|
|
185
219
|
</span>
|
|
186
|
-
{sortBy !== "
|
|
220
|
+
{sortBy !== "pending_risk" && r.total_risked > 0 && (
|
|
187
221
|
<span className="text-[8px] text-white/20 font-medium" style={OUTFIT}>
|
|
188
222
|
· {r.total_risked.toLocaleString()} risked
|
|
189
223
|
</span>
|
|
190
224
|
)}
|
|
191
225
|
{sortBy !== "max_possible" && r.max_possible > 0 && (
|
|
192
226
|
<span className="text-[8px] text-white/20 font-medium" style={OUTFIT}>
|
|
193
|
-
· {r.max_possible.toLocaleString()}
|
|
227
|
+
· {(r.max_possible - startingBalance).toLocaleString()} score
|
|
194
228
|
</span>
|
|
195
229
|
)}
|
|
196
230
|
</div>
|
|
@@ -54,13 +54,15 @@ function getActiveView(
|
|
|
54
54
|
function buildMiniLeaderboard(
|
|
55
55
|
rankings: ITDLeaderboardEntry[],
|
|
56
56
|
userEntryId: number | null,
|
|
57
|
+
startingBalance: number,
|
|
57
58
|
): ILeaderboardEntry[] {
|
|
59
|
+
const score = (r: ITDLeaderboardEntry) => r.max_possible - startingBalance;
|
|
58
60
|
const rows: ILeaderboardEntry[] = [];
|
|
59
61
|
const top = rankings.slice(0, 3);
|
|
60
62
|
top.forEach((r) => {
|
|
61
63
|
rows.push({
|
|
62
64
|
wallet: r.partner_ext_id ?? `User #${r.user_id}`,
|
|
63
|
-
pts: r
|
|
65
|
+
pts: score(r),
|
|
64
66
|
payout: 0,
|
|
65
67
|
isYou: r.entry_id === userEntryId,
|
|
66
68
|
rank: r.rank,
|
|
@@ -72,13 +74,13 @@ function buildMiniLeaderboard(
|
|
|
72
74
|
rows.push({ wallet: "", pts: 0, payout: 0, isYou: false, gapAbove: true });
|
|
73
75
|
if (userIdx > 3) {
|
|
74
76
|
const above = rankings[userIdx - 1];
|
|
75
|
-
rows.push({ wallet: above.partner_ext_id ?? `User #${above.user_id}`, pts: above
|
|
77
|
+
rows.push({ wallet: above.partner_ext_id ?? `User #${above.user_id}`, pts: score(above), payout: 0, isYou: false, rank: userIdx });
|
|
76
78
|
}
|
|
77
79
|
const me = rankings[userIdx];
|
|
78
|
-
rows.push({ wallet: me.partner_ext_id ?? "You", pts: me
|
|
80
|
+
rows.push({ wallet: me.partner_ext_id ?? "You", pts: score(me), payout: 0, isYou: true, rank: userIdx + 1 });
|
|
79
81
|
if (userIdx < rankings.length - 1) {
|
|
80
82
|
const below = rankings[userIdx + 1];
|
|
81
|
-
rows.push({ wallet: below.partner_ext_id ?? `User #${below.user_id}`, pts: below
|
|
83
|
+
rows.push({ wallet: below.partner_ext_id ?? `User #${below.user_id}`, pts: score(below), payout: 0, isYou: false, rank: userIdx + 2 });
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
}
|
|
@@ -228,7 +230,7 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
228
230
|
|
|
229
231
|
const leaderboardRows = useMemo(() => {
|
|
230
232
|
if (poolId && rankings.length > 0) {
|
|
231
|
-
return buildMiniLeaderboard(rankings, entryData?.entry.id ?? null);
|
|
233
|
+
return buildMiniLeaderboard(rankings, entryData?.entry.id ?? null, config?.startingBalance ?? 0);
|
|
232
234
|
}
|
|
233
235
|
if (fallbackPool && config) {
|
|
234
236
|
return buildPoolLeaderboard(fallbackPool, betSummary.potentialBalance, config).rows;
|
|
@@ -76,8 +76,8 @@ export function PreMatchLive({
|
|
|
76
76
|
{entry && (
|
|
77
77
|
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl" style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
|
78
78
|
<div className="flex-1">
|
|
79
|
-
<p className="text-[9px] text-white/40 uppercase tracking-wide" style={OUTFIT}>
|
|
80
|
-
<p className="text-[18px] font-bold text-white" style={OUTFIT}>{entry.
|
|
79
|
+
<p className="text-[9px] text-white/40 uppercase tracking-wide" style={OUTFIT}>Score</p>
|
|
80
|
+
<p className="text-[18px] font-bold text-white" style={OUTFIT}>{(entry.max_possible - config.startingBalance).toLocaleString()} pts</p>
|
|
81
81
|
</div>
|
|
82
82
|
{myRank !== null && myRank > 0 && (
|
|
83
83
|
<div className="text-right">
|
|
@@ -169,7 +169,7 @@ export function PreMatchLive({
|
|
|
169
169
|
key={r.entry_id}
|
|
170
170
|
entry={{
|
|
171
171
|
wallet: r.partner_ext_id ?? `User #${r.user_id}`,
|
|
172
|
-
pts: r.
|
|
172
|
+
pts: r.max_possible - config.startingBalance,
|
|
173
173
|
payout: 0,
|
|
174
174
|
isYou: r.entry_id === entry?.id,
|
|
175
175
|
rank: r.rank,
|