@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
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { useRef, useMemo, useEffect } from "react";
|
|
5
5
|
import { motion } from "framer-motion";
|
|
6
|
-
import { Calendar, MapPin, Loader2, Trophy, Zap, Target, Users
|
|
6
|
+
import { Calendar, MapPin, Loader2, Trophy, Zap, Target, Users } from "lucide-react";
|
|
7
7
|
import { useTDMatches } from "./useTDMatches";
|
|
8
|
+
import { useTDPools } from "../pools/hooks";
|
|
9
|
+
import type { ITDPool } from "../pools/types";
|
|
8
10
|
import type { ITDMatch } from "./types";
|
|
9
11
|
|
|
10
12
|
// TD match status enum values
|
|
@@ -101,9 +103,90 @@ function groupByDate(matches: ITDMatch[]): IMatchDay[] {
|
|
|
101
103
|
}));
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/* ── Pool pills for a match ── */
|
|
107
|
+
|
|
108
|
+
const GAME_TYPE_STYLE: Record<string, { icon: typeof Target; color: string; bg: string }> = {
|
|
109
|
+
pre_match_bets: { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)" },
|
|
110
|
+
fantasy_11: { icon: Users, color: "#9945FF", bg: "rgba(153,69,255,0.06)" },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function PoolPill({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) => void }) {
|
|
114
|
+
const style = GAME_TYPE_STYLE[pool.game_type] ?? { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)" };
|
|
115
|
+
const Icon = style.icon;
|
|
116
|
+
const isClosed = pool.status === "closed" || pool.status === "resolving" || pool.status === "complete";
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => onPress(pool)}
|
|
121
|
+
className="flex items-center gap-1 px-1.5 py-0.5 rounded-md flex-shrink-0"
|
|
122
|
+
style={{
|
|
123
|
+
background: style.bg,
|
|
124
|
+
border: `1px solid ${style.color}20`,
|
|
125
|
+
opacity: isClosed ? 0.5 : 1,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<span className="text-[7px] font-bold" style={{ ...OUTFIT, color: style.color }}>
|
|
129
|
+
{pool.display_price}
|
|
130
|
+
</span>
|
|
131
|
+
{pool.entry_count > 0 && (
|
|
132
|
+
<span className="text-[6px] text-white/30" style={OUTFIT}>·{pool.entry_count > 999 ? `${(pool.entry_count / 1000).toFixed(1)}k` : pool.entry_count}</span>
|
|
133
|
+
)}
|
|
134
|
+
</button>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function MatchGameSection({
|
|
139
|
+
matchId,
|
|
140
|
+
isCompleted,
|
|
141
|
+
onPoolPress,
|
|
142
|
+
partnerSource,
|
|
143
|
+
}: {
|
|
144
|
+
matchId: number;
|
|
145
|
+
isCompleted: boolean;
|
|
146
|
+
onPoolPress?: (pool: ITDPool) => void;
|
|
147
|
+
partnerSource?: string;
|
|
148
|
+
}) {
|
|
149
|
+
const { pools, loading } = useTDPools(matchId, partnerSource);
|
|
150
|
+
|
|
151
|
+
if (isCompleted || loading || pools.length === 0) return null;
|
|
152
|
+
|
|
153
|
+
// Group by game_type
|
|
154
|
+
const grouped: Record<string, ITDPool[]> = {};
|
|
155
|
+
for (const p of pools) {
|
|
156
|
+
(grouped[p.game_type] ??= []).push(p);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const gameTypes = Object.keys(grouped).filter((gt) => gt in GAME_TYPE_STYLE);
|
|
160
|
+
if (gameTypes.length === 0) return null;
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex flex-col gap-1 mt-1 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
|
164
|
+
{gameTypes.map((gt) => {
|
|
165
|
+
const style = GAME_TYPE_STYLE[gt];
|
|
166
|
+
const Icon = style.icon;
|
|
167
|
+
return (
|
|
168
|
+
<div key={gt} className="flex flex-col gap-0.5">
|
|
169
|
+
<div className="flex items-center gap-1 px-1">
|
|
170
|
+
<Icon size={8} style={{ color: style.color }} />
|
|
171
|
+
<span className="text-[7px] barlowcondensedBold tracking-wide text-white/60">
|
|
172
|
+
{gt === "pre_match_bets" ? "Pre-Match Bets" : gt === "fantasy_11" ? "Fantasy 11" : gt}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex gap-1 flex-wrap">
|
|
176
|
+
{grouped[gt].map((pool) => (
|
|
177
|
+
<PoolPill key={pool.id} pool={pool} onPress={onPoolPress ?? (() => {})} />
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
104
187
|
/* ── Match card with border shine animation ── */
|
|
105
188
|
|
|
106
|
-
function MatchCard({ match }: { match: ITDMatch }) {
|
|
189
|
+
function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (pool: ITDPool) => void; partnerSource?: string }) {
|
|
107
190
|
const isLive = match.status === MATCH_STATUS.LIVE;
|
|
108
191
|
const isCompleted = match.status === MATCH_STATUS.COMPLETED;
|
|
109
192
|
const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
|
|
@@ -219,50 +302,13 @@ function MatchCard({ match }: { match: ITDMatch }) {
|
|
|
219
302
|
</div>
|
|
220
303
|
)}
|
|
221
304
|
|
|
222
|
-
{/* Game modes */}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<Target size={9} style={{ color: "#22E3E8" }} />
|
|
230
|
-
<span className="text-[8px] barlowcondensedBold tracking-wide text-white flex-1">
|
|
231
|
-
Pre-Match Bets
|
|
232
|
-
</span>
|
|
233
|
-
<span className="flex items-center gap-0.5">
|
|
234
|
-
<svg width="8" height="8" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#2775CA"/><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"/></svg>
|
|
235
|
-
<span className="text-[7px] text-white" style={OUTFIT}>0.4</span>
|
|
236
|
-
</span>
|
|
237
|
-
</div>
|
|
238
|
-
<div
|
|
239
|
-
className="flex items-center gap-1.5 px-2 py-1 rounded-lg"
|
|
240
|
-
style={{ background: "rgba(153,69,255,0.06)" }}
|
|
241
|
-
>
|
|
242
|
-
<Users size={9} style={{ color: "#9945FF" }} />
|
|
243
|
-
<span className="text-[8px] barlowcondensedBold tracking-wide text-white flex-1">
|
|
244
|
-
Fantasy 11
|
|
245
|
-
</span>
|
|
246
|
-
<span className="flex items-center gap-0.5">
|
|
247
|
-
<svg width="8" height="8" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#2775CA"/><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"/></svg>
|
|
248
|
-
<span className="text-[7px] text-white" style={OUTFIT}>0.4</span>
|
|
249
|
-
</span>
|
|
250
|
-
</div>
|
|
251
|
-
<div
|
|
252
|
-
className="flex items-center gap-1.5 px-2 py-1 rounded-lg"
|
|
253
|
-
style={{ background: "rgba(248,60,197,0.06)" }}
|
|
254
|
-
>
|
|
255
|
-
<Volume2 size={9} style={{ color: "#f83cc5" }} />
|
|
256
|
-
<span className="text-[8px] barlowcondensedBold tracking-wide text-white flex-1">
|
|
257
|
-
Crowd is Wrong
|
|
258
|
-
</span>
|
|
259
|
-
<span className="flex items-center gap-0.5">
|
|
260
|
-
<svg width="8" height="8" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="16" fill="#2775CA"/><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"/></svg>
|
|
261
|
-
<span className="text-[7px] text-white" style={OUTFIT}>0.4</span>
|
|
262
|
-
</span>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
)}
|
|
305
|
+
{/* Game modes — driven by real pools */}
|
|
306
|
+
<MatchGameSection
|
|
307
|
+
matchId={match.id}
|
|
308
|
+
isCompleted={isCompleted}
|
|
309
|
+
onPoolPress={onPoolPress}
|
|
310
|
+
partnerSource={partnerSource}
|
|
311
|
+
/>
|
|
266
312
|
</div>
|
|
267
313
|
</div>
|
|
268
314
|
);
|
|
@@ -270,7 +316,7 @@ function MatchCard({ match }: { match: ITDMatch }) {
|
|
|
270
316
|
|
|
271
317
|
/* ── Day column ── */
|
|
272
318
|
|
|
273
|
-
function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
|
|
319
|
+
function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay; index: number; onPoolPress?: (pool: ITDPool) => void; partnerSource?: string }) {
|
|
274
320
|
const hasToday = day.label === "TODAY";
|
|
275
321
|
const hasLive = day.matches.some((m) => m.status === MATCH_STATUS.LIVE);
|
|
276
322
|
const allCompleted = day.matches.every((m) => m.status === MATCH_STATUS.COMPLETED);
|
|
@@ -312,7 +358,7 @@ function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
|
|
|
312
358
|
|
|
313
359
|
{/* Match cards for the day */}
|
|
314
360
|
{day.matches.map((match) => (
|
|
315
|
-
<MatchCard key={match.id} match={match} />
|
|
361
|
+
<MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
316
362
|
))}
|
|
317
363
|
</motion.div>
|
|
318
364
|
);
|
|
@@ -322,9 +368,13 @@ function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
|
|
|
322
368
|
|
|
323
369
|
interface MatchCalendarProps {
|
|
324
370
|
tournamentId?: number;
|
|
371
|
+
/** Called when user taps a pool pill. Pass poolId + matchId to open the popup. */
|
|
372
|
+
onPoolPress?: (pool: ITDPool) => void;
|
|
373
|
+
/** Filter pools to a specific partner source (e.g. "iamgame") */
|
|
374
|
+
partnerSource?: string;
|
|
325
375
|
}
|
|
326
376
|
|
|
327
|
-
export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
377
|
+
export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
|
|
328
378
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
329
379
|
const { matches, isLoading, error } = useTDMatches({
|
|
330
380
|
tournamentId,
|
|
@@ -428,7 +478,7 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
|
428
478
|
style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
|
|
429
479
|
>
|
|
430
480
|
{days.map((day, i) => (
|
|
431
|
-
<DayColumn key={day.dateKey} day={day} index={i} />
|
|
481
|
+
<DayColumn key={day.dateKey} day={day} index={i} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
432
482
|
))}
|
|
433
483
|
</div>
|
|
434
484
|
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @devrongx/games — pools/actions.ts
|
|
2
|
+
// Imperative API actions for pool participation.
|
|
3
|
+
|
|
4
|
+
import { joinTDPoolApi, placeTDBetsApi } from "./fetcher";
|
|
5
|
+
import type { ITDBetInput } from "./types";
|
|
6
|
+
|
|
7
|
+
// Join a pool. For partner_payout pools, TD verifies the pass with IAG server-to-server.
|
|
8
|
+
// The frontend doesn't need to know the difference.
|
|
9
|
+
export async function joinTDPool(poolId: number): Promise<{ id: number }> {
|
|
10
|
+
return joinTDPoolApi(poolId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Place individual bets. v1: parlay slots are cosmetic only; send individual bets here.
|
|
14
|
+
export async function placeTDBets(
|
|
15
|
+
poolId: number,
|
|
16
|
+
bets: ITDBetInput[],
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
return placeTDBetsApi(poolId, bets);
|
|
19
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @devrongx/games — pools/fetcher.ts
|
|
2
|
+
// Fetch functions for pool endpoints.
|
|
3
|
+
// Reads config (baseURL, getToken) from the same Zustand store as matches/fetcher.ts.
|
|
4
|
+
|
|
5
|
+
import { useTDClientStore } from "../matches/store";
|
|
6
|
+
import type {
|
|
7
|
+
ITDPool,
|
|
8
|
+
ITDPoolDetail,
|
|
9
|
+
ITDMyEntryResponse,
|
|
10
|
+
ITDLeaderboardResponse,
|
|
11
|
+
ITDBetInput,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
// Shared fetch helper — adds auth header if token exists
|
|
15
|
+
async function tdFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
16
|
+
const { baseURL, getToken } = useTDClientStore.getState();
|
|
17
|
+
const url = `${baseURL}${path}`;
|
|
18
|
+
const token = getToken();
|
|
19
|
+
const headers: Record<string, string> = {
|
|
20
|
+
Accept: "application/json",
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
...(init?.headers as Record<string, string>),
|
|
23
|
+
};
|
|
24
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
25
|
+
|
|
26
|
+
const res = await fetch(url, { ...init, headers });
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const body = await res.json().catch(() => ({}));
|
|
29
|
+
throw new Error(
|
|
30
|
+
(body as { error?: { message?: string } }).error?.message ??
|
|
31
|
+
`TD API error: ${res.status} ${res.statusText}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return res.json() as Promise<T>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// GET /api/pools/match/:matchId?partner_source=...
|
|
38
|
+
export async function fetchTDPools(
|
|
39
|
+
matchId: number,
|
|
40
|
+
partnerSource?: string,
|
|
41
|
+
): Promise<ITDPool[]> {
|
|
42
|
+
const qs = partnerSource ? `?partner_source=${encodeURIComponent(partnerSource)}` : "";
|
|
43
|
+
return tdFetch<ITDPool[]>(`/api/pools/match/${matchId}${qs}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// GET /api/pools/:id
|
|
47
|
+
export async function fetchTDPoolDetail(poolId: number): Promise<ITDPoolDetail> {
|
|
48
|
+
return tdFetch<ITDPoolDetail>(`/api/pools/${poolId}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// GET /api/pools/:id/leaderboard
|
|
52
|
+
export async function fetchTDLeaderboard(
|
|
53
|
+
poolId: number,
|
|
54
|
+
params?: { sort?: string; order?: string; limit?: number; offset?: number },
|
|
55
|
+
): Promise<ITDLeaderboardResponse> {
|
|
56
|
+
const url = new URL(`/api/pools/${poolId}/leaderboard`, "http://placeholder");
|
|
57
|
+
if (params) {
|
|
58
|
+
for (const [k, v] of Object.entries(params)) {
|
|
59
|
+
if (v !== undefined) url.searchParams.set(k, String(v));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return tdFetch<ITDLeaderboardResponse>(`/api/pools/${poolId}/leaderboard${url.search}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// GET /api/pools/:id/my-entry
|
|
66
|
+
export async function fetchTDMyEntry(poolId: number): Promise<ITDMyEntryResponse | null> {
|
|
67
|
+
try {
|
|
68
|
+
return await tdFetch<ITDMyEntryResponse>(`/api/pools/${poolId}/my-entry`);
|
|
69
|
+
} catch (err: unknown) {
|
|
70
|
+
// 404 means no entry yet — return null
|
|
71
|
+
if (err instanceof Error && err.message.includes("404")) return null;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// POST /api/pools/:id/join
|
|
77
|
+
export async function joinTDPoolApi(poolId: number): Promise<{ id: number }> {
|
|
78
|
+
return tdFetch<{ id: number }>(`/api/pools/${poolId}/join`, { method: "POST", body: "{}" });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// POST /api/pools/:id/bets
|
|
82
|
+
export async function placeTDBetsApi(
|
|
83
|
+
poolId: number,
|
|
84
|
+
bets: ITDBetInput[],
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
await tdFetch<unknown>(`/api/pools/${poolId}/bets`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: JSON.stringify({ bets }),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @devrongx/games — pools/hooks.ts
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
5
|
+
import {
|
|
6
|
+
fetchTDPools,
|
|
7
|
+
fetchTDPoolDetail,
|
|
8
|
+
fetchTDLeaderboard,
|
|
9
|
+
fetchTDMyEntry,
|
|
10
|
+
} from "./fetcher";
|
|
11
|
+
import type {
|
|
12
|
+
ITDPool,
|
|
13
|
+
ITDPoolDetail,
|
|
14
|
+
ITDMyEntryResponse,
|
|
15
|
+
ITDLeaderboardResponse,
|
|
16
|
+
ITDLeaderboardEntry,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
// ─── useTDPools ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface UseTDPoolsResult {
|
|
22
|
+
pools: ITDPool[];
|
|
23
|
+
loading: boolean;
|
|
24
|
+
error: string | null;
|
|
25
|
+
refetch: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useTDPools(matchId: number, partnerSource?: string): UseTDPoolsResult {
|
|
29
|
+
const [pools, setPools] = useState<ITDPool[]>([]);
|
|
30
|
+
const [loading, setLoading] = useState(true);
|
|
31
|
+
const [error, setError] = useState<string | null>(null);
|
|
32
|
+
|
|
33
|
+
const load = useCallback(async () => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
try {
|
|
37
|
+
const data = await fetchTDPools(matchId, partnerSource);
|
|
38
|
+
setPools(data);
|
|
39
|
+
} catch (err: unknown) {
|
|
40
|
+
setError(err instanceof Error ? err.message : "Failed to fetch pools");
|
|
41
|
+
} finally {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, [matchId, partnerSource]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => { load(); }, [load]);
|
|
47
|
+
|
|
48
|
+
return { pools, loading, error, refetch: load };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── useTDPool ─────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export interface UseTDPoolResult {
|
|
54
|
+
pool: ITDPoolDetail | null;
|
|
55
|
+
loading: boolean;
|
|
56
|
+
error: string | null;
|
|
57
|
+
refetch: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useTDPool(poolId: number): UseTDPoolResult {
|
|
61
|
+
const [pool, setPool] = useState<ITDPoolDetail | null>(null);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [error, setError] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
const load = useCallback(async () => {
|
|
66
|
+
setLoading(true);
|
|
67
|
+
setError(null);
|
|
68
|
+
try {
|
|
69
|
+
const data = await fetchTDPoolDetail(poolId);
|
|
70
|
+
setPool(data);
|
|
71
|
+
} catch (err: unknown) {
|
|
72
|
+
setError(err instanceof Error ? err.message : "Failed to fetch pool");
|
|
73
|
+
} finally {
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
}, [poolId]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => { load(); }, [load]);
|
|
79
|
+
|
|
80
|
+
return { pool, loading, error, refetch: load };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── useTDPoolEntry ─────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export interface UseTDPoolEntryResult {
|
|
86
|
+
data: ITDMyEntryResponse | null;
|
|
87
|
+
loading: boolean;
|
|
88
|
+
error: string | null;
|
|
89
|
+
refetch: () => void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useTDPoolEntry(poolId: number): UseTDPoolEntryResult {
|
|
93
|
+
const [data, setData] = useState<ITDMyEntryResponse | null>(null);
|
|
94
|
+
const [loading, setLoading] = useState(true);
|
|
95
|
+
const [error, setError] = useState<string | null>(null);
|
|
96
|
+
|
|
97
|
+
const load = useCallback(async () => {
|
|
98
|
+
setLoading(true);
|
|
99
|
+
setError(null);
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetchTDMyEntry(poolId);
|
|
102
|
+
setData(res);
|
|
103
|
+
} catch (err: unknown) {
|
|
104
|
+
setError(err instanceof Error ? err.message : "Failed to fetch entry");
|
|
105
|
+
} finally {
|
|
106
|
+
setLoading(false);
|
|
107
|
+
}
|
|
108
|
+
}, [poolId]);
|
|
109
|
+
|
|
110
|
+
useEffect(() => { load(); }, [load]);
|
|
111
|
+
|
|
112
|
+
return { data, loading, error, refetch: load };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── useTDLeaderboard ──────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export interface UseTDLeaderboardOptions {
|
|
118
|
+
sort?: string;
|
|
119
|
+
order?: string;
|
|
120
|
+
limit?: number;
|
|
121
|
+
offset?: number;
|
|
122
|
+
pollMs?: number; // Polling interval in ms (0 = disabled)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface UseTDLeaderboardResult {
|
|
126
|
+
rankings: ITDLeaderboardEntry[];
|
|
127
|
+
total: number;
|
|
128
|
+
loading: boolean;
|
|
129
|
+
error: string | null;
|
|
130
|
+
refetch: () => void;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function useTDLeaderboard(
|
|
134
|
+
poolId: number,
|
|
135
|
+
options?: UseTDLeaderboardOptions,
|
|
136
|
+
): UseTDLeaderboardResult {
|
|
137
|
+
const [result, setResult] = useState<ITDLeaderboardResponse | null>(null);
|
|
138
|
+
const [loading, setLoading] = useState(true);
|
|
139
|
+
const [error, setError] = useState<string | null>(null);
|
|
140
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
141
|
+
|
|
142
|
+
const { sort, order, limit, offset, pollMs } = options ?? {};
|
|
143
|
+
|
|
144
|
+
const load = useCallback(async () => {
|
|
145
|
+
setError(null);
|
|
146
|
+
try {
|
|
147
|
+
const data = await fetchTDLeaderboard(poolId, { sort, order, limit, offset });
|
|
148
|
+
setResult(data);
|
|
149
|
+
} catch (err: unknown) {
|
|
150
|
+
setError(err instanceof Error ? err.message : "Failed to fetch leaderboard");
|
|
151
|
+
} finally {
|
|
152
|
+
setLoading(false);
|
|
153
|
+
}
|
|
154
|
+
}, [poolId, sort, order, limit, offset]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
setLoading(true);
|
|
158
|
+
load();
|
|
159
|
+
|
|
160
|
+
if (pollMs && pollMs > 0) {
|
|
161
|
+
pollRef.current = setInterval(load, pollMs);
|
|
162
|
+
return () => { if (pollRef.current) clearInterval(pollRef.current); };
|
|
163
|
+
}
|
|
164
|
+
}, [load, pollMs]);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
rankings: result?.leaderboard ?? [],
|
|
168
|
+
total: result?.total_entries ?? 0,
|
|
169
|
+
loading,
|
|
170
|
+
error,
|
|
171
|
+
refetch: load,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @devrongx/games — pools/index.ts
|
|
2
|
+
export type {
|
|
3
|
+
ITDCurrency,
|
|
4
|
+
ITDPool,
|
|
5
|
+
ITDPoolDetail,
|
|
6
|
+
ITDPMBConfig,
|
|
7
|
+
ITDPoolContentConfig,
|
|
8
|
+
ITDChallenge,
|
|
9
|
+
ITDChallengeOption,
|
|
10
|
+
ITDPayoutTier,
|
|
11
|
+
ITDLeaderboardEntry,
|
|
12
|
+
ITDLeaderboardResponse,
|
|
13
|
+
ITDPoolEntryRecord,
|
|
14
|
+
ITDPoolBet,
|
|
15
|
+
ITDPoolParlay,
|
|
16
|
+
ITDMyEntryResponse,
|
|
17
|
+
ITDBetInput,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
fetchTDPools,
|
|
22
|
+
fetchTDPoolDetail,
|
|
23
|
+
fetchTDLeaderboard,
|
|
24
|
+
fetchTDMyEntry,
|
|
25
|
+
} from "./fetcher";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
useTDPools,
|
|
29
|
+
useTDPool,
|
|
30
|
+
useTDPoolEntry,
|
|
31
|
+
useTDLeaderboard,
|
|
32
|
+
} from "./hooks";
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
UseTDPoolsResult,
|
|
36
|
+
UseTDPoolResult,
|
|
37
|
+
UseTDPoolEntryResult,
|
|
38
|
+
UseTDLeaderboardResult,
|
|
39
|
+
UseTDLeaderboardOptions,
|
|
40
|
+
} from "./hooks";
|
|
41
|
+
|
|
42
|
+
export { joinTDPool, placeTDBets } from "./actions";
|
|
43
|
+
|
|
44
|
+
export { buildPMBConfig } from "./mapper";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @devrongx/games — pools/mapper.ts
|
|
2
|
+
// Converts ITDPoolDetail + ITDMatch → IChallengeConfig for the PMB UI.
|
|
3
|
+
// This lets all existing PreMatchGame/Questions/Intro components consume real API data.
|
|
4
|
+
|
|
5
|
+
import type { IChallengeConfig, IChallengeMarket, IChallengeSection } from "../games/prematch-bets/config";
|
|
6
|
+
import type { ITDPoolDetail, ITDPMBConfig } from "./types";
|
|
7
|
+
import type { ITDMatch } from "../matches/types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PARLAY_COLORS = ["#22E3E8", "#9945FF", "#f83cc5", "#f59e0b", "#22c55e", "#f97316"];
|
|
10
|
+
const DEFAULT_BANNER = "/ipl/default_banner.jpg";
|
|
11
|
+
const DEFAULT_PLAYER_IMAGES: [string, string] = ["/ipl/player1.jpg", "/ipl/player2.jpg"];
|
|
12
|
+
|
|
13
|
+
function buildTeam(team: ITDMatch["team_a"]) {
|
|
14
|
+
return {
|
|
15
|
+
name: team.name,
|
|
16
|
+
shortName: team.short_name || team.name.slice(0, 3).toUpperCase(),
|
|
17
|
+
logo: `/ipl/${team.short_name?.toLowerCase() ?? "team"}.png`,
|
|
18
|
+
gradient: "linear-gradient(180deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%)",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildPMBConfig(pool: ITDPoolDetail, match: ITDMatch): IChallengeConfig {
|
|
23
|
+
const config = pool.config as ITDPMBConfig;
|
|
24
|
+
const contentConfig = pool.content_config;
|
|
25
|
+
|
|
26
|
+
const markets: IChallengeMarket[] = pool.challenges
|
|
27
|
+
.sort((a, b) => a.sort_order - b.sort_order)
|
|
28
|
+
.map((c) => ({
|
|
29
|
+
id: c.category,
|
|
30
|
+
question: c.question,
|
|
31
|
+
icon: c.icon,
|
|
32
|
+
accent: c.accent_color,
|
|
33
|
+
category: c.category,
|
|
34
|
+
backendChallengeId: c.id,
|
|
35
|
+
options: c.options.map((o) => ({
|
|
36
|
+
label: o.label,
|
|
37
|
+
odds: o.odds,
|
|
38
|
+
entry: o.entry,
|
|
39
|
+
risk: o.risk,
|
|
40
|
+
})),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const sections: IChallengeSection[] = (contentConfig?.sections ?? []).map((s) => ({
|
|
44
|
+
type: s.type as "info" | "challenge",
|
|
45
|
+
title: s.title,
|
|
46
|
+
content: s.content,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id: pool.machine_name ?? String(pool.id),
|
|
51
|
+
title: pool.name,
|
|
52
|
+
matchTitle: `${match.team_a.short_name ?? match.team_a.name} vs ${match.team_b.short_name ?? match.team_b.name}`,
|
|
53
|
+
tag: "STRATEGY",
|
|
54
|
+
teamA: buildTeam(match.team_a),
|
|
55
|
+
teamB: buildTeam(match.team_b),
|
|
56
|
+
venue: match.venue?.name ?? "",
|
|
57
|
+
bannerImage: contentConfig?.banner_image ?? DEFAULT_BANNER,
|
|
58
|
+
playerImages: (contentConfig?.player_images as [string, string] | undefined) ?? DEFAULT_PLAYER_IMAGES,
|
|
59
|
+
opponents: [buildTeam(match.team_a), buildTeam(match.team_b)],
|
|
60
|
+
matchStartTime: match.scheduled_start_at ?? "",
|
|
61
|
+
startingBalance: pool.starting_coins,
|
|
62
|
+
entryFee: pool.entry_fee,
|
|
63
|
+
totalEntries: pool.entry_count,
|
|
64
|
+
rakePercent: pool.rake_percentage / 100,
|
|
65
|
+
rankBrackets: (config.payout_tiers ?? []).map((t) => ({
|
|
66
|
+
from: t.rank_from,
|
|
67
|
+
to: t.rank_to,
|
|
68
|
+
poolPercent: t.payout_type === "percentage" ? t.value / 100 : 0,
|
|
69
|
+
})),
|
|
70
|
+
leaderboardSeed: `pool-${pool.id}`,
|
|
71
|
+
compoundMultipliers: config.compound_multipliers ?? [],
|
|
72
|
+
parlayConfig: {
|
|
73
|
+
maxSlots: config.parlay?.max_slots ?? 6,
|
|
74
|
+
minLegs: config.parlay?.min_legs ?? 2,
|
|
75
|
+
maxLegs: config.parlay?.max_legs ?? 14,
|
|
76
|
+
defaultStake: config.parlay?.default_stake ?? 100,
|
|
77
|
+
stakeIncrements: config.parlay?.stake_increments ?? 50,
|
|
78
|
+
colors: contentConfig?.colors?.parlay_slot_colors ?? DEFAULT_PARLAY_COLORS,
|
|
79
|
+
},
|
|
80
|
+
categoryLabels: config.category_labels ?? {},
|
|
81
|
+
games: [],
|
|
82
|
+
markets,
|
|
83
|
+
sections,
|
|
84
|
+
};
|
|
85
|
+
}
|