@devrongx/games 0.3.5 → 0.4.0

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.
@@ -0,0 +1,192 @@
1
+ // @devrongx/games — games/prematch-bets/PreMatchLive.tsx
2
+ // Live match view: shows challenge resolution progress + live leaderboard.
3
+ // Pool status = CLOSED or RESOLVING.
4
+ "use client";
5
+
6
+ import { useEffect } from "react";
7
+ import { CheckCircle2, Clock, ArrowRight, Radio } from "lucide-react";
8
+ import { IChallengeConfig } from "./config";
9
+ import { LeaderboardRow } from "./LeaderboardRow";
10
+ import { OUTFIT, MARKET_ICONS } from "./constants";
11
+ import type { ITDPoolDetail, ITDMyEntryResponse, ITDLeaderboardEntry } from "../../pools/types";
12
+
13
+ interface PreMatchLiveProps {
14
+ config: IChallengeConfig;
15
+ poolId: number;
16
+ pool: ITDPoolDetail | null;
17
+ entryData: ITDMyEntryResponse | null;
18
+ rankings: ITDLeaderboardEntry[];
19
+ onRefresh: () => void;
20
+ onViewFullLeaderboard: () => void;
21
+ }
22
+
23
+ export function PreMatchLive({
24
+ config,
25
+ pool,
26
+ entryData,
27
+ rankings,
28
+ onRefresh,
29
+ onViewFullLeaderboard,
30
+ }: PreMatchLiveProps) {
31
+ // Poll every 20s during live phase
32
+ useEffect(() => {
33
+ const id = setInterval(onRefresh, 20_000);
34
+ return () => clearInterval(id);
35
+ }, [onRefresh]);
36
+
37
+ const entry = entryData?.entry;
38
+ const myBets = entryData?.bets ?? [];
39
+
40
+ const challenges = pool?.challenges ?? config.markets.map((m, i) => ({
41
+ id: m.backendChallengeId ?? i,
42
+ question: m.question,
43
+ icon: m.icon,
44
+ accent_color: m.accent,
45
+ status: "open",
46
+ correct_option: null as string | null,
47
+ options: m.options.map((o) => ({ key: o.label.toLowerCase().replace(/\s+/g, "_"), label: o.label, odds: o.odds, entry: o.entry, risk: o.risk })),
48
+ category: m.category,
49
+ sort_order: i,
50
+ resolved_at: null as string | null,
51
+ }));
52
+
53
+ const resolvedCount = challenges.filter((c) => c.status === "resolved").length;
54
+ const totalCount = challenges.length;
55
+ const progressPct = totalCount > 0 ? Math.round((resolvedCount / totalCount) * 100) : 0;
56
+
57
+ const myRank = entry ? rankings.findIndex((r) => r.entry_id === entry.id) + 1 : null;
58
+ const miniRankings = rankings.slice(0, 5);
59
+
60
+ return (
61
+ <div className="w-full flex flex-col gap-4 px-4 pb-8">
62
+ {/* Live header */}
63
+ <div className="flex items-center gap-2 pt-5">
64
+ <div className="flex items-center gap-1">
65
+ <span className="relative flex h-2 w-2">
66
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
67
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-[#f83cc5]" />
68
+ </span>
69
+ <span className="text-[11px] font-bold tracking-widest" style={{ color: "#f83cc5", fontFamily: "Barlow Condensed, sans-serif" }}>LIVE</span>
70
+ </div>
71
+ <span className="text-[14px] font-bold text-white" style={OUTFIT}>{config.matchTitle}</span>
72
+ </div>
73
+
74
+ {/* Balance + rank */}
75
+ {entry && (
76
+ <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)" }}>
77
+ <div className="flex-1">
78
+ <p className="text-[9px] text-white/40 uppercase tracking-wide" style={OUTFIT}>Balance</p>
79
+ <p className="text-[18px] font-bold text-white" style={OUTFIT}>{entry.current_coins.toLocaleString()} pts</p>
80
+ </div>
81
+ {myRank !== null && myRank > 0 && (
82
+ <div className="text-right">
83
+ <p className="text-[9px] text-white/40 uppercase tracking-wide" style={OUTFIT}>Your Rank</p>
84
+ <p className="text-[18px] font-bold" style={{ ...OUTFIT, color: "#22E3E8" }}>#{myRank} <span className="text-[11px] text-white/40">of {rankings.length}</span></p>
85
+ </div>
86
+ )}
87
+ </div>
88
+ )}
89
+
90
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
91
+
92
+ {/* Challenge progress */}
93
+ <div>
94
+ <div className="flex items-center justify-between mb-2">
95
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50" style={OUTFIT}>
96
+ RESULTS ({resolvedCount}/{totalCount} resolved)
97
+ </p>
98
+ <span className="text-[9px] font-bold" style={{ ...OUTFIT, color: "#22E3E8" }}>{progressPct}%</span>
99
+ </div>
100
+ <div className="h-[5px] rounded-full mb-3" style={{ background: "rgba(255,255,255,0.06)" }}>
101
+ <div className="h-full rounded-full transition-all duration-700" style={{ width: `${progressPct}%`, background: "linear-gradient(90deg, #22E3E8, #9945FF)" }} />
102
+ </div>
103
+
104
+ <div className="flex flex-col gap-1.5">
105
+ {challenges.map((c) => {
106
+ const Icon = MARKET_ICONS[c.icon] ?? MARKET_ICONS.coin;
107
+ const myBet = myBets.find((b) => b.challenge_id === c.id);
108
+ const isResolved = c.status === "resolved";
109
+ const won = isResolved && myBet?.status === "won";
110
+ const lost = isResolved && myBet?.status === "lost";
111
+
112
+ return (
113
+ <div
114
+ key={c.id}
115
+ className="flex items-start gap-2 px-3 py-2 rounded-lg"
116
+ style={{
117
+ background: isResolved
118
+ ? won ? "rgba(34,197,94,0.06)" : lost ? "rgba(239,68,68,0.06)" : "rgba(255,255,255,0.03)"
119
+ : "rgba(255,255,255,0.03)",
120
+ border: `1px solid ${isResolved ? won ? "rgba(34,197,94,0.2)" : lost ? "rgba(239,68,68,0.2)" : "rgba(255,255,255,0.06)" : "rgba(255,255,255,0.05)"}`,
121
+ }}
122
+ >
123
+ {isResolved ? (
124
+ won
125
+ ? <CheckCircle2 size={12} style={{ color: "#22c55e", flexShrink: 0, marginTop: 1 }} />
126
+ : <span className="text-[10px] flex-shrink-0 mt-0.5" style={{ color: "#ef4444" }}>✗</span>
127
+ ) : (
128
+ <Clock size={12} style={{ color: "rgba(255,255,255,0.25)", flexShrink: 0, marginTop: 1 }} />
129
+ )}
130
+
131
+ <div className="flex-1 min-w-0">
132
+ <p className="text-[10px] text-white/60 font-medium leading-tight" style={OUTFIT}>{c.question}</p>
133
+ {myBet ? (
134
+ <p className="text-[9px] mt-0.5" style={{ ...OUTFIT, color: isResolved ? won ? "#22c55e" : "#ef4444" : "rgba(255,255,255,0.4)" }}>
135
+ {c.options.find((o) => o.key === myBet.selected_option)?.label ?? myBet.selected_option}
136
+ {" @ "}{myBet.odds_at_bet}x — {myBet.coin_amount} pts
137
+ {isResolved && won && ` → +${Math.round(myBet.actual_return ?? myBet.potential_return)} pts`}
138
+ {isResolved && lost && " → Lost"}
139
+ </p>
140
+ ) : (
141
+ <p className="text-[8px] text-white/25 mt-0.5" style={OUTFIT}>No bet placed</p>
142
+ )}
143
+ {isResolved && c.correct_option && (
144
+ <p className="text-[8px] text-white/35 mt-0.5" style={OUTFIT}>
145
+ Answer: {c.options.find((o) => o.key === c.correct_option)?.label ?? c.correct_option}
146
+ </p>
147
+ )}
148
+ {!isResolved && <p className="text-[8px] text-white/25 mt-0.5" style={OUTFIT}>Waiting for resolution…</p>}
149
+ </div>
150
+ </div>
151
+ );
152
+ })}
153
+ </div>
154
+ </div>
155
+
156
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
157
+
158
+ {/* Live leaderboard */}
159
+ {miniRankings.length > 0 && (
160
+ <div>
161
+ <div className="flex items-center gap-1.5 mb-2">
162
+ <Radio size={10} style={{ color: "#f83cc5" }} />
163
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50" style={OUTFIT}>LIVE LEADERBOARD</p>
164
+ </div>
165
+ <div className="flex flex-col">
166
+ {miniRankings.map((r) => (
167
+ <LeaderboardRow
168
+ key={r.entry_id}
169
+ entry={{
170
+ wallet: r.partner_ext_id ?? `User #${r.user_id}`,
171
+ pts: r.current_coins,
172
+ payout: 0,
173
+ isYou: r.entry_id === entry?.id,
174
+ rank: r.rank,
175
+ }}
176
+ rank={r.rank}
177
+ isLast={false}
178
+ />
179
+ ))}
180
+ </div>
181
+ <button
182
+ onClick={onViewFullLeaderboard}
183
+ className="mt-2 flex items-center justify-center gap-1 w-full text-[9px] font-semibold"
184
+ style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
185
+ >
186
+ See Full Leaderboard <ArrowRight size={10} />
187
+ </button>
188
+ </div>
189
+ )}
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,211 @@
1
+ // @devrongx/games — games/prematch-bets/PreMatchResults.tsx
2
+ // Final results view: rank, payout, bet breakdown, stats. Pool status = COMPLETE.
3
+ "use client";
4
+
5
+ import { useMemo } from "react";
6
+ import { Trophy, CheckCircle2, ArrowRight, TrendingUp } from "lucide-react";
7
+ import { IChallengeConfig } from "./config";
8
+ import { LeaderboardRow } from "./LeaderboardRow";
9
+ import { OUTFIT, MARKET_ICONS } from "./constants";
10
+ import type { ITDPoolDetail, ITDMyEntryResponse, ITDLeaderboardEntry } from "../../pools/types";
11
+
12
+ // USDC icon inline
13
+ const UsdcIcon = ({ size = 12 }: { size?: number }) => (
14
+ <svg width={size} height={size} viewBox="0 0 32 32" fill="none">
15
+ <circle cx="16" cy="16" r="16" fill="#2775CA" />
16
+ <path d="M20.5 18.2c0-2.1-1.3-2.8-3.8-3.1-1.8-.3-2.2-.7-2.2-1.5s.7-1.3 1.8-1.3c1 0 1.6.4 1.9 1.1.1.1.2.2.3.2h.7c.2 0 .3-.1.3-.3-.3-1-1-1.8-2.1-2v-1.2c0-.2-.1-.3-.3-.3h-.6c-.2 0-.3.1-.3.3v1.2c-1.5.2-2.5 1.2-2.5 2.4 0 2 1.2 2.7 3.7 3 1.7.3 2.3.8 2.3 1.6 0 1-.8 1.6-2 1.6-1.5 0-2-.6-2.2-1.4 0-.1-.2-.2-.3-.2h-.7c-.2 0-.3.1-.3.3.3 1.2 1 2 2.5 2.3v1.2c0 .2.1.3.3.3h.6c.2 0 .3-.1.3-.3v-1.2c1.6-.2 2.6-1.3 2.6-2.6z" fill="#fff" />
17
+ </svg>
18
+ );
19
+
20
+ interface PreMatchResultsProps {
21
+ config: IChallengeConfig;
22
+ poolId: number;
23
+ pool?: ITDPoolDetail;
24
+ entryData: ITDMyEntryResponse | null;
25
+ rankings: ITDLeaderboardEntry[];
26
+ onViewFullLeaderboard: () => void;
27
+ }
28
+
29
+ export function PreMatchResults({
30
+ config,
31
+ pool,
32
+ entryData,
33
+ rankings,
34
+ onViewFullLeaderboard,
35
+ }: PreMatchResultsProps) {
36
+ const entry = entryData?.entry;
37
+ const myBets = entryData?.bets ?? [];
38
+ const hasPartnerPayout = pool?.currency?.partner_payout ?? false;
39
+
40
+ const myRanking = rankings.find((r) => r.entry_id === entry?.id);
41
+ const totalEntrants = rankings.length;
42
+
43
+ const stats = useMemo(() => {
44
+ const individualBets = myBets.filter((b) => b.parlay_id === null);
45
+ const won = individualBets.filter((b) => b.status === "won").length;
46
+ const lost = individualBets.filter((b) => b.status === "lost").length;
47
+ const totalSpent = individualBets.reduce((s, b) => s + b.coin_amount, 0);
48
+ const totalEarned = entry?.final_coins ?? 0;
49
+ return {
50
+ betsPlaced: individualBets.length,
51
+ totalChallenges: pool?.challenge_count ?? config.markets.length,
52
+ won,
53
+ lost,
54
+ winRate: individualBets.length > 0 ? Math.round((won / individualBets.length) * 100) : 0,
55
+ totalSpent,
56
+ totalEarned,
57
+ roi: totalSpent > 0 ? Math.round(((totalEarned - config.startingBalance) / totalSpent) * 100) : 0,
58
+ };
59
+ }, [myBets, entry, pool, config]);
60
+
61
+ const miniRankings = useMemo(() => {
62
+ const top = rankings.slice(0, 3);
63
+ if (!myRanking || (myRanking.rank ?? 0) <= 3) return top;
64
+ return [...top, myRanking];
65
+ }, [rankings, myRanking]);
66
+
67
+ return (
68
+ <div className="w-full flex flex-col gap-4 px-4 pb-8">
69
+ {/* Header */}
70
+ <div className="flex items-center gap-2 pt-5">
71
+ <Trophy size={18} style={{ color: "#f59e0b" }} />
72
+ <span className="text-[18px] font-bold text-white" style={OUTFIT}>Results</span>
73
+ <span className="text-[12px] text-white/40" style={OUTFIT}>— {config.matchTitle}</span>
74
+ </div>
75
+
76
+ {/* Rank card */}
77
+ {myRanking && (
78
+ <div
79
+ className="relative overflow-hidden rounded-2xl px-4 py-5 flex flex-col items-center gap-2"
80
+ style={{ background: "linear-gradient(135deg, rgba(34,227,232,0.08), rgba(153,69,255,0.08))", border: "1px solid rgba(34,227,232,0.15)" }}
81
+ >
82
+ <p className="text-[11px] text-white/50 uppercase tracking-widest font-semibold" style={OUTFIT}>YOUR FINAL RANK</p>
83
+ <div className="flex items-baseline gap-1">
84
+ <span className="text-[52px] font-bold leading-none" style={{ ...OUTFIT, color: "#22E3E8" }}>#{myRanking.rank}</span>
85
+ <span className="text-[14px] text-white/40" style={OUTFIT}>of {totalEntrants.toLocaleString()}</span>
86
+ </div>
87
+ <p className="text-[13px] font-semibold text-white" style={OUTFIT}>
88
+ Final Score: {(myRanking.final_coins ?? myRanking.current_coins).toLocaleString()} pts
89
+ </p>
90
+ {hasPartnerPayout && myRanking.payout_amount !== null && myRanking.payout_amount !== undefined && (
91
+ <div
92
+ className="flex items-center gap-1.5 px-4 py-2 rounded-xl mt-1"
93
+ style={{ background: "rgba(39,117,202,0.2)", border: "1px solid rgba(39,117,202,0.35)" }}
94
+ >
95
+ <UsdcIcon size={14} />
96
+ <span className="text-[14px] font-bold text-white" style={OUTFIT}>PAYOUT: ${myRanking.payout_amount.toFixed(2)} USDC</span>
97
+ </div>
98
+ )}
99
+ {!hasPartnerPayout && (
100
+ <p className="text-[10px] text-white/30" style={OUTFIT}>
101
+ Earned {(myRanking.final_coins ?? myRanking.current_coins).toLocaleString()} {pool?.currency?.symbol ?? "pts"}
102
+ </p>
103
+ )}
104
+ </div>
105
+ )}
106
+
107
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
108
+
109
+ {/* Bet breakdown */}
110
+ {myBets.length > 0 && (
111
+ <div>
112
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50 mb-2" style={OUTFIT}>
113
+ YOUR BETS — {stats.won} Won, {stats.lost} Lost
114
+ </p>
115
+ <div className="flex flex-col gap-1">
116
+ {myBets.filter((b) => b.parlay_id === null).map((bet) => {
117
+ const market = config.markets.find((m) => m.backendChallengeId === bet.challenge_id);
118
+ const Icon = MARKET_ICONS[market?.icon ?? "coin"];
119
+ const won = bet.status === "won";
120
+ const lost = bet.status === "lost";
121
+
122
+ return (
123
+ <div
124
+ key={bet.id}
125
+ className="flex items-start gap-2 px-3 py-2 rounded-lg"
126
+ style={{
127
+ background: won ? "rgba(34,197,94,0.05)" : lost ? "rgba(239,68,68,0.05)" : "rgba(255,255,255,0.03)",
128
+ border: `1px solid ${won ? "rgba(34,197,94,0.15)" : lost ? "rgba(239,68,68,0.15)" : "rgba(255,255,255,0.05)"}`,
129
+ }}
130
+ >
131
+ {won
132
+ ? <CheckCircle2 size={11} style={{ color: "#22c55e", flexShrink: 0, marginTop: 1 }} />
133
+ : lost
134
+ ? <span className="text-[10px] flex-shrink-0 mt-0.5" style={{ color: "#ef4444" }}>✗</span>
135
+ : Icon && <Icon size={11} style={{ color: market?.accent ?? "#22E3E8", flexShrink: 0, marginTop: 1 }} />
136
+ }
137
+ <div className="flex-1 min-w-0">
138
+ <p className="text-[9px] text-white/60 font-medium leading-tight truncate" style={OUTFIT}>
139
+ {market?.question ?? `Challenge #${bet.challenge_id}`}
140
+ </p>
141
+ <p className="text-[9px] mt-0.5 font-semibold" style={{ ...OUTFIT, color: won ? "#22c55e" : lost ? "#ef4444" : "rgba(255,255,255,0.5)" }}>
142
+ {market?.options.find((o) => o.label.toLowerCase().replace(/\s+/g, "_") === bet.selected_option)?.label ?? bet.selected_option}
143
+ {" @ "}{bet.odds_at_bet}x — {bet.coin_amount} pts
144
+ {won && ` → +${Math.round(bet.actual_return ?? bet.potential_return)} pts`}
145
+ {lost && " → 0 pts"}
146
+ </p>
147
+ </div>
148
+ </div>
149
+ );
150
+ })}
151
+ </div>
152
+ </div>
153
+ )}
154
+
155
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
156
+
157
+ {/* Stats */}
158
+ <div>
159
+ <div className="flex items-center gap-1.5 mb-2">
160
+ <TrendingUp size={11} style={{ color: "#22E3E8" }} />
161
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50" style={OUTFIT}>STATS</p>
162
+ </div>
163
+ <div className="grid grid-cols-2 gap-x-4 gap-y-1">
164
+ {[
165
+ ["Bets placed", `${stats.betsPlaced}/${stats.totalChallenges}`],
166
+ ["Win rate", `${stats.winRate}%`],
167
+ ["Points spent", stats.totalSpent.toLocaleString()],
168
+ ["Final score", stats.totalEarned.toLocaleString()],
169
+ ].map(([label, value]) => (
170
+ <div key={label} className="flex items-center justify-between py-0.5">
171
+ <span className="text-[10px] text-white/40" style={OUTFIT}>{label}</span>
172
+ <span className="text-[10px] font-semibold text-white" style={OUTFIT}>{value}</span>
173
+ </div>
174
+ ))}
175
+ </div>
176
+ </div>
177
+
178
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
179
+
180
+ {/* Final leaderboard */}
181
+ {miniRankings.length > 0 && (
182
+ <div>
183
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50 mb-2" style={OUTFIT}>FINAL LEADERBOARD</p>
184
+ <div className="flex flex-col">
185
+ {miniRankings.map((r) => (
186
+ <LeaderboardRow
187
+ key={r.entry_id}
188
+ entry={{
189
+ wallet: r.partner_ext_id ?? `User #${r.user_id}`,
190
+ pts: r.final_coins ?? r.current_coins,
191
+ payout: 0,
192
+ isYou: r.entry_id === entry?.id,
193
+ rank: r.rank,
194
+ }}
195
+ rank={r.rank}
196
+ isLast={false}
197
+ />
198
+ ))}
199
+ </div>
200
+ <button
201
+ onClick={onViewFullLeaderboard}
202
+ className="mt-2 flex items-center justify-center gap-1 w-full text-[9px] font-semibold"
203
+ style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
204
+ >
205
+ See Full Leaderboard <ArrowRight size={10} />
206
+ </button>
207
+ </div>
208
+ )}
209
+ </div>
210
+ );
211
+ }
@@ -0,0 +1,183 @@
1
+ // @devrongx/games — games/prematch-bets/PreMatchSubmitted.tsx
2
+ // Shows after bets are submitted, while the pool is still OPEN (match hasn't started).
3
+ "use client";
4
+
5
+ import { useState, useEffect } from "react";
6
+ import { CheckCircle2, Clock, ArrowRight, Edit2 } from "lucide-react";
7
+ import { IChallengeConfig } from "./config";
8
+ import { LeaderboardRow } from "./LeaderboardRow";
9
+ import { OUTFIT, MARKET_ICONS } from "./constants";
10
+ import type { ITDMyEntryResponse, ITDLeaderboardEntry } from "../../pools/types";
11
+
12
+ // Countdown hook
13
+ function useCountdown(targetIso: string): string {
14
+ const [text, setText] = useState("");
15
+ useEffect(() => {
16
+ if (!targetIso) return;
17
+ const tick = () => {
18
+ const diff = new Date(targetIso).getTime() - Date.now();
19
+ if (diff <= 0) { setText("LIVE NOW"); return; }
20
+ const d = Math.floor(diff / 86400000);
21
+ const h = Math.floor((diff % 86400000) / 3600000);
22
+ const m = Math.floor((diff % 3600000) / 60000);
23
+ const s = Math.floor((diff % 60000) / 1000);
24
+ if (d > 0) setText(`${d}d ${h}h ${m}m`);
25
+ else if (h > 0) setText(`${h}h ${m}m ${s}s`);
26
+ else setText(`${m}m ${s}s`);
27
+ };
28
+ tick();
29
+ const id = setInterval(tick, 1000);
30
+ return () => clearInterval(id);
31
+ }, [targetIso]);
32
+ return text;
33
+ }
34
+
35
+ interface PreMatchSubmittedProps {
36
+ config: IChallengeConfig;
37
+ poolId: number;
38
+ entryData: ITDMyEntryResponse | null;
39
+ rankings: ITDLeaderboardEntry[];
40
+ onAdjust: () => void;
41
+ onViewLeaderboard: () => void;
42
+ }
43
+
44
+ export function PreMatchSubmitted({
45
+ config,
46
+ entryData,
47
+ rankings,
48
+ onAdjust,
49
+ onViewLeaderboard,
50
+ }: PreMatchSubmittedProps) {
51
+ const countdown = useCountdown(config.matchStartTime);
52
+ const entry = entryData?.entry;
53
+ const bets = entryData?.bets ?? [];
54
+
55
+ // Only show individual (non-parlay) bets
56
+ const individualBets = bets.filter((b) => b.parlay_id === null);
57
+
58
+ const totalSpent = individualBets.reduce((s, b) => s + b.coin_amount, 0);
59
+ const totalPotential = individualBets.reduce((s, b) => s + b.potential_return, 0);
60
+
61
+ // Mini leaderboard (top 3)
62
+ const miniRankings = rankings.slice(0, 5);
63
+
64
+ return (
65
+ <div className="w-full flex flex-col gap-4 px-4 pb-8">
66
+ {/* Success header */}
67
+ <div className="flex flex-col items-center gap-2 pt-6 pb-4">
68
+ <div className="flex items-center justify-center w-12 h-12 rounded-full" style={{ background: "rgba(34,197,94,0.15)" }}>
69
+ <CheckCircle2 size={24} style={{ color: "#22c55e" }} />
70
+ </div>
71
+ <p className="text-[18px] font-bold text-white text-center" style={OUTFIT}>Bets Submitted!</p>
72
+ {countdown && (
73
+ <div className="flex items-center gap-1.5 px-3 py-1 rounded-full" style={{ background: "rgba(255,255,255,0.06)" }}>
74
+ <Clock size={11} style={{ color: "rgba(255,255,255,0.5)" }} />
75
+ <span className="text-[11px] text-white/60" style={OUTFIT}>Match starts in <span className="text-white font-bold">{countdown}</span></span>
76
+ </div>
77
+ )}
78
+ </div>
79
+
80
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
81
+
82
+ {/* Your bets */}
83
+ {individualBets.length > 0 && (
84
+ <div>
85
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50 mb-2" style={OUTFIT}>
86
+ YOUR BETS ({individualBets.length} markets)
87
+ </p>
88
+ <div className="flex flex-col gap-1.5">
89
+ {individualBets.map((bet, i) => {
90
+ const market = config.markets.find((m) => m.backendChallengeId === bet.challenge_id);
91
+ const Icon = MARKET_ICONS[market?.icon ?? "coin"];
92
+ return (
93
+ <div
94
+ key={bet.id}
95
+ className="flex items-center gap-2 px-3 py-2 rounded-lg"
96
+ style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.06)" }}
97
+ >
98
+ {Icon && <Icon size={11} style={{ color: market?.accent ?? "#22E3E8", flexShrink: 0 }} />}
99
+ <span className="flex-1 text-[10px] text-white/70 font-medium truncate" style={OUTFIT}>
100
+ {market?.question ?? `Challenge #${bet.challenge_id}`}
101
+ </span>
102
+ <span className="text-[9px] font-bold" style={{ ...OUTFIT, color: "#22E3E8" }}>
103
+ {bet.odds_at_bet}x
104
+ </span>
105
+ <span className="text-[9px] text-white/50" style={OUTFIT}>—</span>
106
+ <span className="text-[9px] font-semibold text-white" style={OUTFIT}>{bet.coin_amount} pts</span>
107
+ </div>
108
+ );
109
+ })}
110
+ </div>
111
+ </div>
112
+ )}
113
+
114
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
115
+
116
+ {/* Summary */}
117
+ {entry && (
118
+ <div className="flex flex-col gap-1">
119
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50 mb-1" style={OUTFIT}>Summary</p>
120
+ <div className="flex items-center justify-between">
121
+ <span className="text-[11px] text-white/50" style={OUTFIT}>Points spent</span>
122
+ <span className="text-[11px] font-semibold text-white" style={OUTFIT}>{totalSpent} / {config.startingBalance.toLocaleString()}</span>
123
+ </div>
124
+ <div className="flex items-center justify-between">
125
+ <span className="text-[11px] text-white/50" style={OUTFIT}>Points remaining</span>
126
+ <span className="text-[11px] font-semibold text-white" style={OUTFIT}>{entry.current_coins.toLocaleString()}</span>
127
+ </div>
128
+ <div className="flex items-center justify-between">
129
+ <span className="text-[11px] text-white/50" style={OUTFIT}>Max potential</span>
130
+ <span className="text-[11px] font-bold" style={{ ...OUTFIT, color: "#22E3E8" }}>{Math.round(totalPotential + entry.current_coins).toLocaleString()}</span>
131
+ </div>
132
+ </div>
133
+ )}
134
+
135
+ {/* Adjust bets button */}
136
+ <button
137
+ onClick={onAdjust}
138
+ className="flex items-center justify-center gap-1.5 w-full py-2.5 rounded-xl"
139
+ style={{ background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.1)" }}
140
+ >
141
+ <Edit2 size={12} style={{ color: "rgba(255,255,255,0.5)" }} />
142
+ <span className="text-[11px] font-semibold text-white/60" style={OUTFIT}>Adjust Bets</span>
143
+ </button>
144
+
145
+ <div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
146
+
147
+ {/* Mini leaderboard */}
148
+ {miniRankings.length > 0 && (
149
+ <div>
150
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-white/50 mb-2" style={OUTFIT}>Leaderboard</p>
151
+ <div className="flex items-center gap-2 px-3 mb-1">
152
+ <span className="w-[28px] text-right text-[8px] text-white/30 uppercase tracking-wide" style={OUTFIT}>#</span>
153
+ <span className="flex-1 text-[8px] text-white/30 uppercase tracking-wide" style={OUTFIT}>Player</span>
154
+ <span className="w-[52px] text-right text-[8px] text-white/30 uppercase tracking-wide" style={OUTFIT}>Pts</span>
155
+ </div>
156
+ <div className="flex flex-col">
157
+ {miniRankings.map((r) => (
158
+ <LeaderboardRow
159
+ key={r.entry_id}
160
+ entry={{
161
+ wallet: r.partner_ext_id ?? `User #${r.user_id}`,
162
+ pts: r.current_coins,
163
+ payout: 0,
164
+ isYou: false,
165
+ rank: r.rank,
166
+ }}
167
+ rank={r.rank}
168
+ isLast={false}
169
+ />
170
+ ))}
171
+ </div>
172
+ <button
173
+ onClick={onViewLeaderboard}
174
+ className="mt-2 flex items-center justify-center gap-1 w-full text-[9px] font-semibold"
175
+ style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
176
+ >
177
+ See Full Leaderboard <ArrowRight size={10} />
178
+ </button>
179
+ </div>
180
+ )}
181
+ </div>
182
+ );
183
+ }
@@ -20,6 +20,7 @@ export interface IChallengeMarket {
20
20
  icon: string;
21
21
  accent: string;
22
22
  category: string; // "toss" | "match" | "csk_innings" | "rcb_innings" | "match_level"
23
+ backendChallengeId?: number; // Maps to challenges.id in the TD DB — required for API bet placement
23
24
  options: IChallengeOption[];
24
25
  userBet?: IBetEntry; // Present when API returns a logged-in user's saved bet for this market
25
26
  }
package/src/index.ts CHANGED
@@ -12,3 +12,19 @@ export type { IChallengeConfig, IChallengeMarket, IChallengeOption, IChallengeTe
12
12
  // Matches — TD match calendar, data fetching, and types
13
13
  export { configureTDClient, fetchTDMatches, useTDMatches, MatchCalendar } from "./matches";
14
14
  export type { ITDClientConfig, ITDMatch, ITDTeam, ITDPlayer, ITDVenue, ITDTournament, ITDStage, ITDMatchesResponse, ITDMatchesParams, UseTDMatchesResult } from "./matches";
15
+
16
+ // Pools — real API data layer
17
+ export {
18
+ useTDPools, useTDPool, useTDPoolEntry, useTDLeaderboard,
19
+ joinTDPool, placeTDBets,
20
+ fetchTDPools, fetchTDPoolDetail, fetchTDLeaderboard, fetchTDMyEntry,
21
+ buildPMBConfig,
22
+ } from "./pools";
23
+ export type {
24
+ ITDPool, ITDPoolDetail, ITDChallenge, ITDChallengeOption, ITDCurrency,
25
+ ITDPMBConfig, ITDPoolContentConfig, ITDPayoutTier,
26
+ ITDLeaderboardEntry, ITDLeaderboardResponse,
27
+ ITDPoolEntryRecord, ITDPoolBet, ITDPoolParlay, ITDMyEntryResponse,
28
+ ITDBetInput,
29
+ UseTDPoolsResult, UseTDPoolResult, UseTDPoolEntryResult, UseTDLeaderboardResult, UseTDLeaderboardOptions,
30
+ } from "./pools";