@devrongx/games 0.3.4 → 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.
- package/package.json +1 -1
- package/src/games/prematch-bets/FullLeaderboard.tsx +149 -0
- package/src/games/prematch-bets/PreMatchBetsPopup.tsx +306 -76
- package/src/games/prematch-bets/PreMatchGame.tsx +64 -8
- package/src/games/prematch-bets/PreMatchLive.tsx +192 -0
- package/src/games/prematch-bets/PreMatchQuestions.tsx +66 -42
- package/src/games/prematch-bets/PreMatchResults.tsx +211 -0
- package/src/games/prematch-bets/PreMatchSubmitted.tsx +183 -0
- package/src/games/prematch-bets/config.ts +1 -0
- package/src/games/prematch-bets/constants.tsx +39 -1
- package/src/index.ts +16 -0
- package/src/matches/MatchCalendar.tsx +100 -50
- package/src/pools/actions.ts +19 -0
- package/src/pools/fetcher.ts +90 -0
- package/src/pools/hooks.ts +173 -0
- package/src/pools/index.ts +44 -0
- package/src/pools/mapper.ts +85 -0
- package/src/pools/types.ts +194 -0
|
@@ -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
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Shared constants and micro-components for IPL betting UI.
|
|
3
3
|
|
|
4
4
|
import Image from "next/image";
|
|
5
|
-
import { Coins, Trophy, Swords, Square, Zap, Target, BarChart3, Star } from "lucide-react";
|
|
5
|
+
import { Coins, Trophy, Swords, Square, Zap, Target, BarChart3, Star, ArrowUpRight } from "lucide-react";
|
|
6
6
|
|
|
7
7
|
/** Font style applied to all betting UI text */
|
|
8
8
|
export const OUTFIT = { fontFamily: "Outfit, sans-serif" } as const;
|
|
@@ -31,3 +31,41 @@ export const SelectedCheck = ({ size = 10 }: { size?: number }) => (
|
|
|
31
31
|
<path d="M5 8l2 2 4-4" stroke="#9945FF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
32
32
|
</svg>
|
|
33
33
|
);
|
|
34
|
+
|
|
35
|
+
/** Build iplgpt.com URL with question + options pre-filled */
|
|
36
|
+
export const buildIplGptUrl = (question: string, options: string[]) => {
|
|
37
|
+
const params = new URLSearchParams({ q: question, opts: options.join(",") });
|
|
38
|
+
return `https://www.iplgpt.com/?${params.toString()}`;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* AI Insights button — solid cyan, floating rounded pill, Sparkles icon only.
|
|
43
|
+
* Redirects to iplgpt.com with question + options pre-filled in the query string.
|
|
44
|
+
*/
|
|
45
|
+
export const AiInsightButton = ({
|
|
46
|
+
question,
|
|
47
|
+
options,
|
|
48
|
+
onClick,
|
|
49
|
+
}: {
|
|
50
|
+
question: string;
|
|
51
|
+
options: string[];
|
|
52
|
+
onClick?: (e: React.MouseEvent) => void;
|
|
53
|
+
}) => (
|
|
54
|
+
<a
|
|
55
|
+
href={buildIplGptUrl(question, options)}
|
|
56
|
+
target="_blank"
|
|
57
|
+
rel="noopener noreferrer"
|
|
58
|
+
onClick={(e) => { e.stopPropagation(); onClick?.(e); }}
|
|
59
|
+
className="inline-flex items-center gap-[3px] select-none"
|
|
60
|
+
style={{
|
|
61
|
+
borderBottom: "1px dotted rgba(34,227,232,0.9)",
|
|
62
|
+
paddingBottom: 2,
|
|
63
|
+
textDecoration: "none",
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<span className="text-[11px] font-semibold" style={{ ...OUTFIT, color: "#22E3E8", letterSpacing: "0.02em" }}>
|
|
67
|
+
Expert Insight
|
|
68
|
+
</span>
|
|
69
|
+
<ArrowUpRight size={12} style={{ color: "#22E3E8", flexShrink: 0 }} />
|
|
70
|
+
</a>
|
|
71
|
+
);
|
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";
|