@devrongx/games 0.4.48 → 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,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.4.48",
3
+ "version": "0.4.50",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -1,13 +1,14 @@
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
 
7
8
  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" },
9
+ { bg: "#FFD700", text: "#000" },
10
+ { bg: "#C0C0C0", text: "#000" },
11
+ { bg: "#CD7F32", text: "#000" },
11
12
  ];
12
13
 
13
14
  interface LeaderboardRowProps {
@@ -22,11 +23,14 @@ export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) =>
22
23
  const rankStyle = isTop3 ? RANK_COLORS[rankIdx] : null;
23
24
 
24
25
  return (
25
- <div
26
- className="relative flex items-center gap-2 px-3 py-[6px] transition-all duration-300"
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
- borderBottom: isLast ? "none" : "1px solid rgba(255,255,255,0.03)",
29
- background: entry.isYou ? "rgba(34,227,232,0.06)" : "transparent",
32
+ borderBottom: isLast ? "none" : "1px solid rgba(255,255,255,0.08)",
33
+ background: entry.isYou ? "rgba(34,227,232,0.08)" : "#000",
30
34
  }}
31
35
  >
32
36
  {entry.isYou && (
@@ -36,17 +40,17 @@ export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) =>
36
40
  <div className="w-[32px] flex-shrink-0 flex items-center justify-center">
37
41
  {isTop3 && rankStyle ? (
38
42
  <span
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 }}>
43
+ className="inline-flex items-center justify-center w-[22px] h-[22px] rounded-full text-[10px] font-extrabold"
44
+ style={{ background: rankStyle.bg, color: rankStyle.text, ...OUTFIT }}>
41
45
  {rank}
42
46
  </span>
43
47
  ) : (
44
- <span className="text-[10px] text-white/40 font-semibold" style={OUTFIT}>{rank.toLocaleString()}</span>
48
+ <span className="text-[10px] text-white/50 font-semibold" style={OUTFIT}>{rank.toLocaleString()}</span>
45
49
  )}
46
50
  </div>
47
51
  <div className="flex items-center gap-1.5 flex-1 min-w-0">
48
52
  <span
49
- className={`text-[11px] truncate font-semibold ${entry.isYou ? "text-[#22E3E8]" : isTop3 ? "text-white" : "text-white/60"}`}
53
+ className={`text-[11px] truncate font-semibold ${entry.isYou ? "text-[#22E3E8]" : "text-white"}`}
50
54
  style={OUTFIT}>
51
55
  {entry.wallet}
52
56
  </span>
@@ -60,11 +64,11 @@ export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) =>
60
64
  <div className="w-[66px] flex-shrink-0 flex items-center justify-end gap-[3px]">
61
65
  <PointsIcon size={9} />
62
66
  <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)" } : {}) }}>
67
+ className="text-[11px] font-bold"
68
+ style={{ ...OUTFIT, color: "#22E3E8", ...(entry.isYou ? { textShadow: "0 0 6px rgba(34,227,232,0.3)" } : {}) }}>
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
- const top = rankings.slice(0, 3);
62
- top.forEach((r) => {
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: r.partner_ext_id ?? `User #${r.user_id}`,
65
- pts: score(r),
95
+ wallet: player.isYou ? "You" : player.wallet,
96
+ pts: player.pts,
66
97
  payout: 0,
67
- isYou: r.entry_id === userEntryId,
68
- rank: r.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
- return buildMiniLeaderboard(rankings, entryData?.entry.id ?? null, config?.startingBalance ?? 0);
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
- {/* Combined Bets (Parlays) — hidden until v2 */}
715
- {false && <div className="mt-4">
716
- {hasAnyParlays ? (
717
- <>
718
- <div className="flex items-center gap-2 mb-1">
719
- <p className="text-[9px] text-white/60 font-semibold uppercase tracking-wider" style={OUTFIT}>
720
- Combined Bets
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
- <div className="flex items-center justify-between gap-3">
819
- <div className="flex-1 min-w-0">
820
- <p className="text-[18px] font-bold text-white leading-tight" style={OUTFIT}>
821
- Now combine bets for crazy multipliers
822
- </p>
823
- {!parlayEnabled && (
824
- <span className="inline-block mt-1.5 text-[7px] font-bold px-1.5 py-0.5 rounded-full" style={{ background: "rgba(153,69,255,0.15)", color: "#9945FF", border: "1px solid rgba(153,69,255,0.3)" }}>
825
- Coming in v2
826
- </span>
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}>&middot;&middot;&middot;</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
- <button
830
- onClick={parlayEnabled ? () => setEditingParlay(0) : undefined}
831
- className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
832
- style={{ background: "linear-gradient(135deg, rgba(153,69,255,0.2), rgba(248,60,197,0.2), rgba(34,227,232,0.2))", cursor: parlayEnabled ? "pointer" : "default", opacity: parlayEnabled ? 1 : 0.5 }}>
833
- <div className="absolute inset-y-0 left-0 w-[30%]" style={{ background: "linear-gradient(135deg, #9945FF, #f83cc5, #22E3E8)" }} />
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
- </div>
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/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>
848
- </div>
849
- <div className="h-px bg-white/5 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}>&middot;&middot;&middot;</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>