@devrongx/games 0.3.5 → 0.4.1

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,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";
@@ -3,8 +3,11 @@
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";
10
+ import { TDGameType, TDPoolStatus } from "../pools/types";
8
11
  import type { ITDMatch } from "./types";
9
12
 
10
13
  // TD match status enum values
@@ -101,9 +104,93 @@ function groupByDate(matches: ITDMatch[]): IMatchDay[] {
101
104
  }));
102
105
  }
103
106
 
107
+ /* ── Pool pills for a match ── */
108
+
109
+ const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string; bg: string; label: string }> = {
110
+ [TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)", label: "Pre-Match Bets" },
111
+ [TDGameType.FANTASY_11]: { icon: Users, color: "#9945FF", bg: "rgba(153,69,255,0.06)", label: "Fantasy 11" },
112
+ [TDGameType.BALL_SEQUENCE]: { icon: Zap, color: "#FF9945", bg: "rgba(255,153,69,0.06)", label: "Ball Sequence" },
113
+ };
114
+
115
+ function PoolPill({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) => void }) {
116
+ const style = GAME_TYPE_STYLE[pool.game_type] ?? { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)" };
117
+ const Icon = style.icon;
118
+ const isClosed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
119
+
120
+ return (
121
+ <button
122
+ onClick={() => onPress(pool)}
123
+ className="flex items-center gap-1 px-1.5 py-0.5 rounded-md flex-shrink-0"
124
+ style={{
125
+ background: style.bg,
126
+ border: `1px solid ${style.color}20`,
127
+ opacity: isClosed ? 0.5 : 1,
128
+ }}
129
+ >
130
+ <span className="text-[7px] font-bold" style={{ ...OUTFIT, color: style.color }}>
131
+ {pool.display_price}
132
+ </span>
133
+ {pool.entry_count > 0 && (
134
+ <span className="text-[6px] text-white/30" style={OUTFIT}>·{pool.entry_count > 999 ? `${(pool.entry_count / 1000).toFixed(1)}k` : pool.entry_count}</span>
135
+ )}
136
+ </button>
137
+ );
138
+ }
139
+
140
+ function MatchGameSection({
141
+ matchId,
142
+ isCompleted,
143
+ onPoolPress,
144
+ partnerSource,
145
+ }: {
146
+ matchId: number;
147
+ isCompleted: boolean;
148
+ onPoolPress?: (pool: ITDPool) => void;
149
+ partnerSource?: string;
150
+ }) {
151
+ const { pools, loading } = useTDPools(matchId, partnerSource);
152
+
153
+ if (isCompleted || loading || pools.length === 0) return null;
154
+
155
+ // Group by game_type (integer)
156
+ const grouped: Record<number, ITDPool[]> = {};
157
+ for (const p of pools) {
158
+ (grouped[p.game_type] ??= []).push(p);
159
+ }
160
+
161
+ const gameTypes = (Object.keys(grouped) as unknown as number[])
162
+ .map(Number)
163
+ .filter((gt) => gt in GAME_TYPE_STYLE);
164
+ if (gameTypes.length === 0) return null;
165
+
166
+ return (
167
+ <div className="flex flex-col gap-1 mt-1 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
168
+ {gameTypes.map((gt) => {
169
+ const style = GAME_TYPE_STYLE[gt];
170
+ const Icon = style.icon;
171
+ return (
172
+ <div key={gt} className="flex flex-col gap-0.5">
173
+ <div className="flex items-center gap-1 px-1">
174
+ <Icon size={8} style={{ color: style.color }} />
175
+ <span className="text-[7px] barlowcondensedBold tracking-wide text-white/60">
176
+ {style.label}
177
+ </span>
178
+ </div>
179
+ <div className="flex gap-1 flex-wrap">
180
+ {grouped[gt].map((pool) => (
181
+ <PoolPill key={pool.id} pool={pool} onPress={onPoolPress ?? (() => {})} />
182
+ ))}
183
+ </div>
184
+ </div>
185
+ );
186
+ })}
187
+ </div>
188
+ );
189
+ }
190
+
104
191
  /* ── Match card with border shine animation ── */
105
192
 
106
- function MatchCard({ match }: { match: ITDMatch }) {
193
+ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (pool: ITDPool) => void; partnerSource?: string }) {
107
194
  const isLive = match.status === MATCH_STATUS.LIVE;
108
195
  const isCompleted = match.status === MATCH_STATUS.COMPLETED;
109
196
  const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
@@ -219,50 +306,13 @@ function MatchCard({ match }: { match: ITDMatch }) {
219
306
  </div>
220
307
  )}
221
308
 
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
- )}
309
+ {/* Game modes — driven by real pools */}
310
+ <MatchGameSection
311
+ matchId={match.id}
312
+ isCompleted={isCompleted}
313
+ onPoolPress={onPoolPress}
314
+ partnerSource={partnerSource}
315
+ />
266
316
  </div>
267
317
  </div>
268
318
  );
@@ -270,7 +320,7 @@ function MatchCard({ match }: { match: ITDMatch }) {
270
320
 
271
321
  /* ── Day column ── */
272
322
 
273
- function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
323
+ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay; index: number; onPoolPress?: (pool: ITDPool) => void; partnerSource?: string }) {
274
324
  const hasToday = day.label === "TODAY";
275
325
  const hasLive = day.matches.some((m) => m.status === MATCH_STATUS.LIVE);
276
326
  const allCompleted = day.matches.every((m) => m.status === MATCH_STATUS.COMPLETED);
@@ -312,7 +362,7 @@ function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
312
362
 
313
363
  {/* Match cards for the day */}
314
364
  {day.matches.map((match) => (
315
- <MatchCard key={match.id} match={match} />
365
+ <MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
316
366
  ))}
317
367
  </motion.div>
318
368
  );
@@ -322,9 +372,13 @@ function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
322
372
 
323
373
  interface MatchCalendarProps {
324
374
  tournamentId?: number;
375
+ /** Called when user taps a pool pill. Pass poolId + matchId to open the popup. */
376
+ onPoolPress?: (pool: ITDPool) => void;
377
+ /** Filter pools to a specific partner source (e.g. "iamgame") */
378
+ partnerSource?: string;
325
379
  }
326
380
 
327
- export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
381
+ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
328
382
  const scrollRef = useRef<HTMLDivElement>(null);
329
383
  const { matches, isLoading, error } = useTDMatches({
330
384
  tournamentId,
@@ -428,7 +482,7 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
428
482
  style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
429
483
  >
430
484
  {days.map((day, i) => (
431
- <DayColumn key={day.dateKey} day={day} index={i} />
485
+ <DayColumn key={day.dateKey} day={day} index={i} onPoolPress={onPoolPress} partnerSource={partnerSource} />
432
486
  ))}
433
487
  </div>
434
488
  </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
+ }