@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.
@@ -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, Volume2 } from "lucide-react";
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
- {!isCompleted && (
224
- <div className="flex flex-col gap-1 mt-1 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
225
- <div
226
- className="flex items-center gap-1.5 px-2 py-1 rounded-lg"
227
- style={{ background: "rgba(34,227,232,0.06)" }}
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
+ }