@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.
- package/package.json +1 -1
- package/src/games/prematch-bets/FullLeaderboard.tsx +149 -0
- package/src/games/prematch-bets/PreMatchBetsPopup.tsx +307 -76
- package/src/games/prematch-bets/PreMatchGame.tsx +55 -7
- package/src/games/prematch-bets/PreMatchLive.tsx +193 -0
- package/src/games/prematch-bets/PreMatchResults.tsx +212 -0
- package/src/games/prematch-bets/PreMatchSubmitted.tsx +183 -0
- package/src/games/prematch-bets/config.ts +1 -0
- package/src/index.ts +16 -0
- package/src/matches/MatchCalendar.tsx +104 -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 +229 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
-
)}
|
|
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
|
+
}
|