@devrongx/games 0.4.36 → 0.4.38
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/matches/MatchCalendar.tsx +60 -50
- package/src/matches/types.ts +1 -0
- package/src/pools/fetcher.ts +2 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import { useRef, useMemo, useEffect } from "react";
|
|
5
|
-
import { motion
|
|
5
|
+
import { motion } from "framer-motion";
|
|
6
6
|
import { Calendar, MapPin, Loader2, Trophy, Zap, Target, Users } from "lucide-react";
|
|
7
7
|
import { useTDMatches } from "./useTDMatches";
|
|
8
8
|
import { useTDPools } from "../pools/hooks";
|
|
@@ -68,6 +68,17 @@ function getShineOpacity(popularity: number): number {
|
|
|
68
68
|
return Math.min(popularity / 10, 1);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function humanizeCount(n: number): string {
|
|
72
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
73
|
+
return String(n);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Determine if a pool is free based on display_price and entry_fee */
|
|
77
|
+
function isFreePool(pool: ITDPool): boolean {
|
|
78
|
+
const dp = (pool.display_price || "").toLowerCase().trim();
|
|
79
|
+
return dp === "" || dp === "free" || (pool.entry_fee === 0 && dp === "");
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
/* ── Grouping: by date ── */
|
|
72
83
|
|
|
73
84
|
interface IMatchDay {
|
|
@@ -104,7 +115,7 @@ function groupByDate(matches: ITDMatch[]): IMatchDay[] {
|
|
|
104
115
|
}));
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
/* ── Pool
|
|
118
|
+
/* ── Pool row item ── */
|
|
108
119
|
|
|
109
120
|
const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string; bg: string; label: string }> = {
|
|
110
121
|
[TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)", label: "Pre-Match Bets" },
|
|
@@ -112,37 +123,40 @@ const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string; bg:
|
|
|
112
123
|
[TDGameType.BALL_SEQUENCE]: { icon: Zap, color: "#FF9945", bg: "rgba(255,153,69,0.06)", label: "Ball Sequence" },
|
|
113
124
|
};
|
|
114
125
|
|
|
115
|
-
function
|
|
126
|
+
function PoolRow({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) => void }) {
|
|
116
127
|
const style = GAME_TYPE_STYLE[pool.game_type] ?? { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)", label: "Play" };
|
|
117
128
|
const isClosed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
|
|
118
129
|
const isOpen = pool.status === TDPoolStatus.OPEN;
|
|
130
|
+
const free = isFreePool(pool);
|
|
119
131
|
|
|
120
132
|
return (
|
|
121
133
|
<motion.button
|
|
122
134
|
onClick={() => onPress(pool)}
|
|
123
|
-
whileTap={{ scale: 0.
|
|
124
|
-
|
|
125
|
-
transition={{ type: "spring", stiffness: 500, damping: 25 }}
|
|
126
|
-
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg flex-shrink-0"
|
|
135
|
+
whileTap={{ scale: 0.96 }}
|
|
136
|
+
className="flex items-center gap-1.5 w-full px-2 py-1.5 rounded-lg"
|
|
127
137
|
style={{
|
|
128
|
-
background: isOpen ? style.bg.replace("0.06", "0.
|
|
129
|
-
border: `1px solid ${style.color}${isOpen ? "
|
|
138
|
+
background: isOpen ? style.bg.replace("0.06", "0.08") : style.bg,
|
|
139
|
+
border: `1px solid ${style.color}${isOpen ? "30" : "18"}`,
|
|
130
140
|
opacity: isClosed ? 0.45 : 1,
|
|
131
|
-
boxShadow: isOpen ? `0 0 8px ${style.color}18` : undefined,
|
|
132
141
|
}}
|
|
133
142
|
>
|
|
143
|
+
{/* Live dot */}
|
|
134
144
|
{isOpen && (
|
|
135
145
|
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
|
136
146
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-60" style={{ backgroundColor: style.color }} />
|
|
137
147
|
<span className="relative inline-flex rounded-full h-1.5 w-1.5" style={{ backgroundColor: style.color }} />
|
|
138
148
|
</span>
|
|
139
149
|
)}
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
|
|
151
|
+
{/* Label: "Play for Free" or cost */}
|
|
152
|
+
<span className="flex-1 text-left text-[9px] font-bold tracking-wide" style={{ ...OUTFIT, color: free ? "rgba(255,255,255,0.9)" : "#fff" }}>
|
|
153
|
+
{free ? "Play for Free" : pool.display_price}
|
|
142
154
|
</span>
|
|
155
|
+
|
|
156
|
+
{/* Entry count for non-free pools, or always if there are entries */}
|
|
143
157
|
{pool.entry_count > 0 && (
|
|
144
|
-
<span className="text-[8px] font-medium" style={{ ...OUTFIT, color: `${style.color}99` }}>
|
|
145
|
-
{
|
|
158
|
+
<span className="text-[8px] font-medium shrink-0" style={{ ...OUTFIT, color: `${style.color}99` }}>
|
|
159
|
+
{humanizeCount(pool.entry_count)} in
|
|
146
160
|
</span>
|
|
147
161
|
)}
|
|
148
162
|
</motion.button>
|
|
@@ -176,27 +190,37 @@ function MatchGameSection({
|
|
|
176
190
|
if (gameTypes.length === 0) return null;
|
|
177
191
|
|
|
178
192
|
return (
|
|
179
|
-
<div className="flex flex-col gap-
|
|
193
|
+
<div className="flex flex-col gap-2 mt-1.5 pt-2" style={{ borderTop: "1px solid rgba(255,255,255,0.08)" }}>
|
|
180
194
|
{gameTypes.map((gt) => {
|
|
181
195
|
const style = GAME_TYPE_STYLE[gt];
|
|
182
196
|
const Icon = style.icon;
|
|
197
|
+
// Sort: free pools first, then by display_price ascending
|
|
198
|
+
const sorted = [...grouped[gt]].sort((a, b) => {
|
|
199
|
+
const aFree = isFreePool(a) ? 0 : 1;
|
|
200
|
+
const bFree = isFreePool(b) ? 0 : 1;
|
|
201
|
+
if (aFree !== bFree) return aFree - bFree;
|
|
202
|
+
return a.entry_fee - b.entry_fee;
|
|
203
|
+
});
|
|
204
|
+
|
|
183
205
|
return (
|
|
184
206
|
<div key={gt} className="flex flex-col gap-1">
|
|
185
|
-
|
|
207
|
+
{/* Game type pill header */}
|
|
208
|
+
<div className="flex items-center gap-1 px-0.5 mb-0.5">
|
|
186
209
|
<Icon size={9} style={{ color: style.color }} />
|
|
187
|
-
<span className="text-[8px] barlowcondensedBold tracking-wide" style={{ color:
|
|
210
|
+
<span className="text-[8px] barlowcondensedBold tracking-wide" style={{ color: style.color }}>
|
|
188
211
|
{style.label}
|
|
189
212
|
</span>
|
|
190
213
|
</div>
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
{/* Pool rows */}
|
|
215
|
+
<div className="flex flex-col gap-1">
|
|
216
|
+
{sorted.map((pool, idx) => (
|
|
193
217
|
<motion.div
|
|
194
218
|
key={pool.id}
|
|
195
|
-
initial={{ opacity: 0, y:
|
|
219
|
+
initial={{ opacity: 0, y: 4 }}
|
|
196
220
|
animate={{ opacity: 1, y: 0 }}
|
|
197
|
-
transition={{ delay: idx * 0.
|
|
221
|
+
transition={{ delay: idx * 0.03, duration: 0.2, ease: "easeOut" }}
|
|
198
222
|
>
|
|
199
|
-
<
|
|
223
|
+
<PoolRow pool={pool} onPress={onPoolPress ?? (() => {})} />
|
|
200
224
|
</motion.div>
|
|
201
225
|
))}
|
|
202
226
|
</div>
|
|
@@ -245,7 +269,7 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
|
|
|
245
269
|
)}
|
|
246
270
|
|
|
247
271
|
<motion.div
|
|
248
|
-
className="relative flex flex-col gap-
|
|
272
|
+
className="relative flex flex-col gap-1.5 rounded-xl px-3 py-2.5"
|
|
249
273
|
style={{
|
|
250
274
|
background: isLive
|
|
251
275
|
? "linear-gradient(160deg, rgba(14,10,24,0.98) 0%, rgba(10,8,20,0.98) 100%)"
|
|
@@ -257,10 +281,10 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
|
|
|
257
281
|
{/* Time + live indicator */}
|
|
258
282
|
<div className="flex items-center justify-between gap-1">
|
|
259
283
|
<span
|
|
260
|
-
className="text-[
|
|
284
|
+
className="text-[10px] font-semibold"
|
|
261
285
|
style={{
|
|
262
286
|
...OUTFIT,
|
|
263
|
-
color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "
|
|
287
|
+
color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "rgba(255,255,255,0.7)",
|
|
264
288
|
}}
|
|
265
289
|
>
|
|
266
290
|
{time}
|
|
@@ -286,16 +310,16 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
|
|
|
286
310
|
{/* Teams */}
|
|
287
311
|
<div className="flex items-center gap-2 justify-center">
|
|
288
312
|
<span
|
|
289
|
-
className="barlowcondensedBold text-[
|
|
313
|
+
className="barlowcondensedBold text-[20px] leading-none tracking-wide"
|
|
290
314
|
style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
|
|
291
315
|
>
|
|
292
316
|
{getTeamShortName(match, "a")}
|
|
293
317
|
</span>
|
|
294
|
-
<span className="text-[
|
|
318
|
+
<span className="text-[9px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.45)" }}>
|
|
295
319
|
vs
|
|
296
320
|
</span>
|
|
297
321
|
<span
|
|
298
|
-
className="barlowcondensedBold text-[
|
|
322
|
+
className="barlowcondensedBold text-[20px] leading-none tracking-wide"
|
|
299
323
|
style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
|
|
300
324
|
>
|
|
301
325
|
{getTeamShortName(match, "b")}
|
|
@@ -305,30 +329,16 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
|
|
|
305
329
|
{/* Venue */}
|
|
306
330
|
{venue && (
|
|
307
331
|
<div className="flex items-center gap-1 justify-center">
|
|
308
|
-
<MapPin size={
|
|
332
|
+
<MapPin size={8} style={{ color: "rgba(255,255,255,0.45)" }} />
|
|
309
333
|
<span
|
|
310
|
-
className="text-[
|
|
311
|
-
style={{ ...OUTFIT, color: "rgba(255,255,255,0.
|
|
334
|
+
className="text-[8px] truncate max-w-[120px]"
|
|
335
|
+
style={{ ...OUTFIT, color: "rgba(255,255,255,0.6)" }}
|
|
312
336
|
>
|
|
313
337
|
{venue}
|
|
314
338
|
</span>
|
|
315
339
|
</div>
|
|
316
340
|
)}
|
|
317
341
|
|
|
318
|
-
{/* Popularity indicator */}
|
|
319
|
-
{match.rating_popularity > 0 && !isCompleted && (
|
|
320
|
-
<div className="flex items-center gap-0.5 justify-center mt-0.5">
|
|
321
|
-
{Array.from({ length: Math.min(Math.ceil(match.rating_popularity / 2), 5) }).map((_, i) => (
|
|
322
|
-
<Zap
|
|
323
|
-
key={i}
|
|
324
|
-
size={8}
|
|
325
|
-
fill={isLive ? "#f83cc5" : "#22E3E8"}
|
|
326
|
-
style={{ color: isLive ? "#f83cc5" : "#22E3E8", opacity: 0.45 + i * 0.13 }}
|
|
327
|
-
/>
|
|
328
|
-
))}
|
|
329
|
-
</div>
|
|
330
|
-
)}
|
|
331
|
-
|
|
332
342
|
{/* Game modes — driven by real pools */}
|
|
333
343
|
<MatchGameSection
|
|
334
344
|
matchId={match.id}
|
|
@@ -354,12 +364,12 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
|
|
|
354
364
|
animate={{ opacity: 1, y: 0 }}
|
|
355
365
|
transition={{ duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] }}
|
|
356
366
|
className="shrink-0 flex flex-col gap-2"
|
|
357
|
-
style={{ scrollSnapAlign: "start", width: "
|
|
367
|
+
style={{ scrollSnapAlign: "start", width: "160px" }}
|
|
358
368
|
>
|
|
359
369
|
{/* Day header */}
|
|
360
|
-
<div className="flex flex-col items-center gap-0.5 pb-1
|
|
370
|
+
<div className="flex flex-col items-center gap-0.5 pb-1">
|
|
361
371
|
<span
|
|
362
|
-
className="text-[
|
|
372
|
+
className="text-[10px] barlowcondensedBold tracking-widest"
|
|
363
373
|
style={{
|
|
364
374
|
color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.9)",
|
|
365
375
|
}}
|
|
@@ -367,7 +377,7 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
|
|
|
367
377
|
{day.label}
|
|
368
378
|
</span>
|
|
369
379
|
<span
|
|
370
|
-
className="text-[
|
|
380
|
+
className="text-[12px] font-semibold"
|
|
371
381
|
style={{
|
|
372
382
|
...OUTFIT,
|
|
373
383
|
color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "#fff",
|
|
@@ -377,7 +387,7 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
|
|
|
377
387
|
</span>
|
|
378
388
|
{(hasToday || hasLive) && (
|
|
379
389
|
<div
|
|
380
|
-
className="w-
|
|
390
|
+
className="w-5 h-[2px] rounded-full mt-0.5"
|
|
381
391
|
style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
|
|
382
392
|
/>
|
|
383
393
|
)}
|
package/src/matches/types.ts
CHANGED
package/src/pools/fetcher.ts
CHANGED
|
@@ -40,7 +40,8 @@ export async function fetchTDPools(
|
|
|
40
40
|
matchId: number,
|
|
41
41
|
partnerSource?: string,
|
|
42
42
|
): Promise<ITDPool[]> {
|
|
43
|
-
const
|
|
43
|
+
const source = partnerSource ?? useTDClientStore.getState().partnerSource;
|
|
44
|
+
const qs = source ? `?partner_source=${encodeURIComponent(source)}` : "";
|
|
44
45
|
return tdFetch<ITDPool[]>(`/api/pools/match/${matchId}${qs}`);
|
|
45
46
|
}
|
|
46
47
|
|