@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.4.35",
3
+ "version": "0.4.36",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -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, Wallet } from "lucide-react";
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, 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" },
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 "total_risked": return entry.total_risked;
30
- case "current_coins": return entry.current_coins;
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" : ""} · {activeTab.desc}
80
+ {total.toLocaleString()} player{total !== 1 ? "s" : ""}
73
81
  </p>
74
82
  </div>
75
83
  </div>
76
84
 
77
- {/* Sort tabs — pill style matching game FilterPill */}
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 !== "total_risked" && r.total_risked > 0 && (
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()} max
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.max_possible,
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.current_coins, payout: 0, isYou: false, rank: userIdx });
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.current_coins, payout: 0, isYou: true, rank: userIdx + 1 });
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.current_coins, payout: 0, isYou: false, rank: userIdx + 2 });
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}>Balance</p>
80
- <p className="text-[18px] font-bold text-white" style={OUTFIT}>{entry.current_coins.toLocaleString()} pts</p>
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.current_coins,
172
+ pts: r.max_possible - config.startingBalance,
173
173
  payout: 0,
174
174
  isYou: r.entry_id === entry?.id,
175
175
  rank: r.rank,
@@ -147,6 +147,7 @@ export interface ITDLeaderboardEntry {
147
147
  final_coins: number | null;
148
148
  total_risked: number;
149
149
  max_possible: number;
150
+ pending_risk: number;
150
151
  bets_placed: number;
151
152
  bets_won: number;
152
153
  bets_lost: number;