@devrongx/games 0.4.49 → 0.4.50
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,7 @@
|
|
|
1
1
|
// @devrongx/games — games/prematch-bets/LeaderboardRow.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
+
import { motion } from "framer-motion";
|
|
4
5
|
import { ILeaderboardEntry } from "./config";
|
|
5
6
|
import { OUTFIT, PointsIcon } from "./constants";
|
|
6
7
|
|
|
@@ -22,8 +23,11 @@ export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) =>
|
|
|
22
23
|
const rankStyle = isTop3 ? RANK_COLORS[rankIdx] : null;
|
|
23
24
|
|
|
24
25
|
return (
|
|
25
|
-
<div
|
|
26
|
-
|
|
26
|
+
<motion.div
|
|
27
|
+
layout
|
|
28
|
+
layoutId={entry.isYou ? "lb-you" : `lb-${entry.wallet}-${entry.rank}`}
|
|
29
|
+
transition={{ layout: { type: "spring", stiffness: 300, damping: 30 } }}
|
|
30
|
+
className="relative flex items-center gap-2 px-3 py-[6px]"
|
|
27
31
|
style={{
|
|
28
32
|
borderBottom: isLast ? "none" : "1px solid rgba(255,255,255,0.08)",
|
|
29
33
|
background: entry.isYou ? "rgba(34,227,232,0.08)" : "#000",
|
|
@@ -65,6 +69,6 @@ export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) =>
|
|
|
65
69
|
{entry.pts.toLocaleString()}
|
|
66
70
|
</span>
|
|
67
71
|
</div>
|
|
68
|
-
</div>
|
|
72
|
+
</motion.div>
|
|
69
73
|
);
|
|
70
74
|
};
|
|
@@ -55,34 +55,50 @@ function buildMiniLeaderboard(
|
|
|
55
55
|
rankings: ITDLeaderboardEntry[],
|
|
56
56
|
userEntryId: number | null,
|
|
57
57
|
startingBalance: number,
|
|
58
|
+
pendingPts?: number,
|
|
58
59
|
): ILeaderboardEntry[] {
|
|
59
60
|
const score = (r: ITDLeaderboardEntry) => r.max_possible - startingBalance;
|
|
61
|
+
|
|
62
|
+
// Build sorted list with pending user score override
|
|
63
|
+
const userPendingScore = pendingPts != null ? pendingPts : undefined;
|
|
64
|
+
|
|
65
|
+
// Create a sorted array of {rank, score, isYou, wallet} based on pending score
|
|
66
|
+
type Ranked = { wallet: string; pts: number; isYou: boolean; originalIdx: number };
|
|
67
|
+
const allPlayers: Ranked[] = rankings.map((r, i) => ({
|
|
68
|
+
wallet: r.partner_ext_id ?? `User #${r.user_id}`,
|
|
69
|
+
pts: (r.entry_id === userEntryId && userPendingScore !== undefined) ? userPendingScore : score(r),
|
|
70
|
+
isYou: r.entry_id === userEntryId,
|
|
71
|
+
originalIdx: i,
|
|
72
|
+
}));
|
|
73
|
+
allPlayers.sort((a, b) => b.pts - a.pts);
|
|
74
|
+
|
|
75
|
+
// Assign ranks
|
|
76
|
+
const userRank = allPlayers.findIndex(p => p.isYou) + 1;
|
|
77
|
+
const totalPlayers = allPlayers.length;
|
|
78
|
+
|
|
79
|
+
// Build visible set: top 3 + user neighborhood
|
|
80
|
+
const rankSet = new Set<number>();
|
|
81
|
+
for (let r = 1; r <= Math.min(3, totalPlayers); r++) rankSet.add(r);
|
|
82
|
+
if (userRank > 0) {
|
|
83
|
+
for (let r = Math.max(1, userRank - 1); r <= Math.min(totalPlayers, userRank + 1); r++) {
|
|
84
|
+
rankSet.add(r);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const visibleRanks = [...rankSet].sort((a, b) => a - b);
|
|
88
|
+
|
|
60
89
|
const rows: ILeaderboardEntry[] = [];
|
|
61
|
-
|
|
62
|
-
|
|
90
|
+
for (let i = 0; i < visibleRanks.length; i++) {
|
|
91
|
+
const rank = visibleRanks[i];
|
|
92
|
+
const player = allPlayers[rank - 1];
|
|
93
|
+
const gapAbove = i > 0 && visibleRanks[i - 1] < rank - 1;
|
|
63
94
|
rows.push({
|
|
64
|
-
wallet:
|
|
65
|
-
pts:
|
|
95
|
+
wallet: player.isYou ? "You" : player.wallet,
|
|
96
|
+
pts: player.pts,
|
|
66
97
|
payout: 0,
|
|
67
|
-
isYou:
|
|
68
|
-
rank
|
|
98
|
+
isYou: player.isYou,
|
|
99
|
+
rank,
|
|
100
|
+
gapAbove,
|
|
69
101
|
});
|
|
70
|
-
});
|
|
71
|
-
if (userEntryId !== null) {
|
|
72
|
-
const userIdx = rankings.findIndex((r) => r.entry_id === userEntryId);
|
|
73
|
-
if (userIdx >= 3) {
|
|
74
|
-
rows.push({ wallet: "", pts: 0, payout: 0, isYou: false, gapAbove: true });
|
|
75
|
-
if (userIdx > 3) {
|
|
76
|
-
const above = rankings[userIdx - 1];
|
|
77
|
-
rows.push({ wallet: above.partner_ext_id ?? `User #${above.user_id}`, pts: score(above), payout: 0, isYou: false, rank: userIdx });
|
|
78
|
-
}
|
|
79
|
-
const me = rankings[userIdx];
|
|
80
|
-
rows.push({ wallet: me.partner_ext_id ?? "You", pts: score(me), payout: 0, isYou: true, rank: userIdx + 1 });
|
|
81
|
-
if (userIdx < rankings.length - 1) {
|
|
82
|
-
const below = rankings[userIdx + 1];
|
|
83
|
-
rows.push({ wallet: below.partner_ext_id ?? `User #${below.user_id}`, pts: score(below), payout: 0, isYou: false, rank: userIdx + 2 });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
102
|
}
|
|
87
103
|
return rows;
|
|
88
104
|
}
|
|
@@ -230,13 +246,15 @@ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp
|
|
|
230
246
|
|
|
231
247
|
const leaderboardRows = useMemo(() => {
|
|
232
248
|
if (poolId && rankings.length > 0) {
|
|
233
|
-
|
|
249
|
+
// Pass pending max outcome so user's rank updates as they adjust bets
|
|
250
|
+
const pendingPts = betSummary.compoundedReward > 0 ? betSummary.compoundedReward : undefined;
|
|
251
|
+
return buildMiniLeaderboard(rankings, entryData?.entry.id ?? null, config?.startingBalance ?? 0, pendingPts);
|
|
234
252
|
}
|
|
235
253
|
if (fallbackPool && config) {
|
|
236
254
|
return buildPoolLeaderboard(fallbackPool, betSummary.potentialBalance, config).rows;
|
|
237
255
|
}
|
|
238
256
|
return [];
|
|
239
|
-
}, [poolId, rankings, entryData, fallbackPool, config, betSummary.potentialBalance]);
|
|
257
|
+
}, [poolId, rankings, entryData, fallbackPool, config, betSummary.potentialBalance, betSummary.compoundedReward]);
|
|
240
258
|
|
|
241
259
|
// ── View routing ──────────────────────────────────────────────────────────
|
|
242
260
|
const hasEntry = !!entryData?.entry;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
5
5
|
import Image from "next/image";
|
|
6
|
-
import { motion, AnimatePresence, useSpring, useTransform, useMotionValue } from "framer-motion";
|
|
6
|
+
import { motion, AnimatePresence, LayoutGroup, useSpring, useTransform, useMotionValue } from "framer-motion";
|
|
7
7
|
import { ChevronDown, Info, X, Play, Pencil, Loader2, Check, EyeOff } from "lucide-react";
|
|
8
8
|
import { IBetSummary, ILeaderboardEntry, IChallengeConfig, IUserBets, deriveParlayGroups, deriveMarketToParlay, calcDisplayReward, optionReward, calcParlayMultiplier } from "./config";
|
|
9
9
|
import { OUTFIT, MARKET_ICONS, PointsIcon, SelectedCheck, AiInsightButton, FilterPill } from "./constants";
|
|
@@ -711,159 +711,36 @@ export const PreMatchGame = ({
|
|
|
711
711
|
)}
|
|
712
712
|
</AnimatePresence>
|
|
713
713
|
|
|
714
|
-
{/*
|
|
715
|
-
{
|
|
716
|
-
{
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
</p>
|
|
722
|
-
</div>
|
|
723
|
-
<p className="text-[7px] text-white/30 font-medium mb-2.5" style={OUTFIT}>
|
|
724
|
-
Every leg must hit. One miss and the entire combined bet is lost.
|
|
725
|
-
</p>
|
|
726
|
-
|
|
727
|
-
<div className="flex items-end gap-3">
|
|
728
|
-
{Array.from({ length: config.parlayConfig.maxSlots }, (_, slot) => {
|
|
729
|
-
const legs = parlayGroups[slot] ?? [];
|
|
730
|
-
const legCount = legs.length;
|
|
731
|
-
const isEditing = editingParlay === slot;
|
|
732
|
-
const color = parlayColors[slot];
|
|
733
|
-
const { multiplier, totalStake, breakdown } = legCount > 0
|
|
734
|
-
? calcParlayMultiplier(slot, bets, config)
|
|
735
|
-
: { multiplier: 0, totalStake: 0, breakdown: [] };
|
|
736
|
-
|
|
737
|
-
return (
|
|
738
|
-
<div key={slot} className="flex flex-col items-center gap-1">
|
|
739
|
-
<button onClick={() => setEditingParlay(isEditing ? null : slot)}
|
|
740
|
-
className="relative flex items-center justify-center transition-all" style={{ width: 36, height: 36 }}>
|
|
741
|
-
{legCount === 0 ? (
|
|
742
|
-
<div className="w-[28px] h-[28px] rounded-full border border-dashed transition-all"
|
|
743
|
-
style={{ borderColor: isEditing ? color : "rgba(255,255,255,0.15)", boxShadow: isEditing ? `0 0 8px ${color}40` : "none" }} />
|
|
744
|
-
) : (
|
|
745
|
-
<>
|
|
746
|
-
{Array.from({ length: Math.min(legCount, 6) }, (_, ring) => {
|
|
747
|
-
const size = 12 + ring * 5;
|
|
748
|
-
const opacity = 1 - ring * 0.12;
|
|
749
|
-
return (
|
|
750
|
-
<motion.div key={ring} initial={{ scale: 0 }} animate={{ scale: 1 }}
|
|
751
|
-
transition={{ type: "spring", stiffness: 400, damping: 20, delay: ring * 0.05 }}
|
|
752
|
-
className="absolute rounded-full"
|
|
753
|
-
style={{ width: size, height: size, border: `2px solid ${color}`, opacity, background: ring === 0 ? `${color}30` : "transparent", boxShadow: isEditing ? `0 0 8px ${color}40` : "none" }} />
|
|
754
|
-
);
|
|
755
|
-
})}
|
|
756
|
-
</>
|
|
757
|
-
)}
|
|
758
|
-
</button>
|
|
759
|
-
{legCount >= 2 ? (
|
|
760
|
-
<div className="flex items-center gap-0.5">
|
|
761
|
-
<span className="text-[8px] font-bold text-white" style={OUTFIT}>{multiplier}x</span>
|
|
762
|
-
<button onClick={() => setParlayInfo(parlayInfo === slot ? null : slot)}>
|
|
763
|
-
<Info size={8} className="text-white/40" />
|
|
764
|
-
</button>
|
|
765
|
-
</div>
|
|
766
|
-
) : legCount === 1 ? (
|
|
767
|
-
<span className="text-[7px] text-white/30 font-medium" style={OUTFIT}>+1 more</span>
|
|
768
|
-
) : null}
|
|
769
|
-
<AnimatePresence>
|
|
770
|
-
{parlayInfo === slot && legCount >= 2 && (
|
|
771
|
-
<motion.div initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 4 }}
|
|
772
|
-
className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 w-[180px] rounded-lg p-2.5"
|
|
773
|
-
style={{ background: "#1a1a2e", border: `1px solid ${color}30` }}>
|
|
774
|
-
<div className="flex items-center justify-between mb-1.5">
|
|
775
|
-
<span className="text-[8px] text-white font-bold" style={OUTFIT}>Combined Bet</span>
|
|
776
|
-
<button onClick={() => setParlayInfo(null)}><X size={10} className="text-white/40" /></button>
|
|
777
|
-
</div>
|
|
778
|
-
{breakdown.map((leg, i) => (
|
|
779
|
-
<div key={i} className="flex items-center justify-between py-0.5">
|
|
780
|
-
<span className="text-[7px] text-white/60 font-medium truncate flex-1 pr-1" style={OUTFIT}>{leg.question}</span>
|
|
781
|
-
<span className="text-[8px] text-white font-semibold flex-shrink-0" style={OUTFIT}>×{leg.odds}</span>
|
|
782
|
-
</div>
|
|
783
|
-
))}
|
|
784
|
-
<div className="h-px bg-white/10 my-1" />
|
|
785
|
-
<div className="flex items-center justify-between">
|
|
786
|
-
<span className="text-[7px] text-white/40 font-medium" style={OUTFIT}>Combined</span>
|
|
787
|
-
<span className="text-[9px] font-bold" style={{ ...OUTFIT, color }}>{multiplier}x</span>
|
|
788
|
-
</div>
|
|
789
|
-
{totalStake > 0 && (
|
|
790
|
-
<div className="flex items-center justify-between mt-0.5">
|
|
791
|
-
<span className="text-[7px] text-white/40 font-medium" style={OUTFIT}>Stake → Win</span>
|
|
792
|
-
<span className="text-[8px] text-white font-semibold" style={OUTFIT}>{totalStake} → {Math.round(totalStake * multiplier).toLocaleString()}</span>
|
|
793
|
-
</div>
|
|
794
|
-
)}
|
|
795
|
-
<p className="text-[6px] text-white/25 mt-1" style={OUTFIT}>All must hit to win</p>
|
|
796
|
-
<button onClick={() => { clearParlay(slot); setParlayInfo(null); }}
|
|
797
|
-
className="mt-1.5 text-[7px] text-red-400/60 font-medium" style={OUTFIT}>Remove combined bet</button>
|
|
798
|
-
</motion.div>
|
|
799
|
-
)}
|
|
800
|
-
</AnimatePresence>
|
|
801
|
-
</div>
|
|
802
|
-
);
|
|
803
|
-
})}
|
|
804
|
-
</div>
|
|
805
|
-
{editingParlay !== null && (
|
|
806
|
-
<div className="flex items-center justify-between mt-2">
|
|
807
|
-
<p className="text-[8px] font-medium" style={{ ...OUTFIT, color: parlayColors[editingParlay] }}>
|
|
808
|
-
Tap questions above to add to this combined bet
|
|
809
|
-
</p>
|
|
810
|
-
<button onClick={() => setEditingParlay(null)}
|
|
811
|
-
className="text-[8px] text-white/50 font-semibold px-2 py-0.5 rounded" style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)" }}>
|
|
812
|
-
Done
|
|
813
|
-
</button>
|
|
714
|
+
{/* Leaderboard (inside sticky) */}
|
|
715
|
+
{leaderboardRows.length > 0 && (
|
|
716
|
+
<div className="mt-3 rounded-xl overflow-hidden" style={{ border: "1px solid rgba(255,255,255,0.08)" }}>
|
|
717
|
+
<div className="flex items-center gap-2 px-3 py-1.5" style={{ background: "rgba(255,255,255,0.03)", borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
|
|
718
|
+
<span className="w-[32px] flex-shrink-0 text-center text-[8px] text-white/50 uppercase tracking-widest font-bold" style={OUTFIT}>#</span>
|
|
719
|
+
<span className="flex-1 text-[8px] text-white/50 uppercase tracking-widest font-bold pl-2" style={OUTFIT}>Player</span>
|
|
720
|
+
<span className="w-[66px] flex-shrink-0 text-right text-[8px] text-white/50 uppercase tracking-widest font-bold" style={OUTFIT}>Score</span>
|
|
814
721
|
</div>
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
)}
|
|
722
|
+
<LayoutGroup>
|
|
723
|
+
<div className="flex flex-col">
|
|
724
|
+
{leaderboardRows.map((entry, i, arr) => (
|
|
725
|
+
<div key={entry.isYou ? "you" : `${entry.wallet}-${entry.rank}`}>
|
|
726
|
+
{entry.gapAbove && (
|
|
727
|
+
<div className="flex items-center justify-center py-0.5" style={{ background: "#000" }}>
|
|
728
|
+
<span className="text-[8px] text-white/20 font-semibold tracking-widest" style={OUTFIT}>···</span>
|
|
729
|
+
</div>
|
|
730
|
+
)}
|
|
731
|
+
<LeaderboardRow entry={entry} rank={entry.rank ?? i + 1} isLast={i === arr.length - 1} />
|
|
732
|
+
</div>
|
|
733
|
+
))}
|
|
828
734
|
</div>
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
style={{
|
|
833
|
-
|
|
834
|
-
<Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
|
|
735
|
+
</LayoutGroup>
|
|
736
|
+
{onViewLeaderboard && (
|
|
737
|
+
<button onClick={onViewLeaderboard} className="w-full text-center text-[9px] font-semibold py-1.5"
|
|
738
|
+
style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)", background: "rgba(255,255,255,0.02)", borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
|
739
|
+
See Full Leaderboard →
|
|
835
740
|
</button>
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
</div>}
|
|
839
|
-
</div>
|
|
840
|
-
|
|
841
|
-
{/* Leaderboard & Potential Payouts */}
|
|
842
|
-
<div className="pt-1">
|
|
843
|
-
<p className="text-[10px] text-white uppercase tracking-wide mb-2 font-semibold" style={OUTFIT}>Leaderboard</p>
|
|
844
|
-
<div className="flex items-center gap-2 px-3 mb-1">
|
|
845
|
-
<span className="w-[32px] flex-shrink-0 text-center text-[8px] text-white/50 uppercase tracking-widest font-bold" style={OUTFIT}>#</span>
|
|
846
|
-
<span className="flex-1 text-[8px] text-white/50 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/50 uppercase tracking-widest font-bold" style={OUTFIT}>Score</span>
|
|
848
|
-
</div>
|
|
849
|
-
<div className="h-px bg-white/10 mb-1" />
|
|
850
|
-
<div className="flex flex-col">
|
|
851
|
-
{leaderboardRows.map((entry, i, arr) => (
|
|
852
|
-
<div key={entry.wallet + (entry.rank ?? i)}>
|
|
853
|
-
{entry.gapAbove && (
|
|
854
|
-
<div className="flex items-center justify-center py-0.5">
|
|
855
|
-
<span className="text-[8px] text-white/20 font-semibold tracking-widest" style={OUTFIT}>···</span>
|
|
856
|
-
</div>
|
|
857
|
-
)}
|
|
858
|
-
<LeaderboardRow entry={entry} rank={entry.rank ?? i + 1} isLast={i === arr.length - 1} />
|
|
859
|
-
</div>
|
|
860
|
-
))}
|
|
861
|
-
</div>
|
|
862
|
-
{onViewLeaderboard && leaderboardRows.length > 0 && (
|
|
863
|
-
<button onClick={onViewLeaderboard} className="mt-2 w-full text-center text-[9px] font-semibold"
|
|
864
|
-
style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}>See Full Leaderboard →</button>
|
|
741
|
+
)}
|
|
742
|
+
</div>
|
|
865
743
|
)}
|
|
866
|
-
</div>
|
|
867
744
|
</>
|
|
868
745
|
)}
|
|
869
746
|
</div>
|