@devrongx/games 0.4.3 → 0.4.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -21,8 +21,8 @@ import { PreMatchLive } from "./PreMatchLive";
21
21
  import { PreMatchResults } from "./PreMatchResults";
22
22
  import { FullLeaderboard } from "./FullLeaderboard";
23
23
  import { useTDPool, useTDPoolEntry, useTDLeaderboard } from "../../pools/hooks";
24
- import { useTDMatches } from "../../matches/useTDMatches";
25
24
  import { buildPMBConfig } from "../../pools/mapper";
25
+ import type { ITDMatch } from "../../matches/types";
26
26
  import { joinTDPool, placeTDBets } from "../../pools/actions";
27
27
  import type { ITDLeaderboardEntry } from "../../pools/types";
28
28
  import { TDPoolStatus } from "../../pools/types";
@@ -89,27 +89,28 @@ interface PreMatchBetsPopupProps {
89
89
  /** When provided, fetches real data. When absent, uses hardcoded CSK_VS_RCB demo. */
90
90
  poolId?: number;
91
91
  matchId?: number;
92
+ /** Pass the ITDMatch directly (from MatchCalendar's onPoolPress) to skip a redundant fetch. */
93
+ match?: ITDMatch;
92
94
  }
93
95
 
94
96
  // ─── Component ───────────────────────────────────────────────────────────────
95
97
 
96
- export const PreMatchBetsPopup = ({ poolId, matchId }: PreMatchBetsPopupProps) => {
98
+ export const PreMatchBetsPopup = ({ poolId, matchId: _matchId, match: matchProp }: PreMatchBetsPopupProps) => {
97
99
  const { activeView: storeView } = useGamePopupStore();
98
100
 
99
101
  // ── Real API data (only when poolId provided) ────────────────────────────
100
102
  const { pool, loading: poolLoading, refetch: refetchPool } = useTDPool(poolId ?? 0);
101
103
  const { data: entryData, refetch: refetchEntry } = useTDPoolEntry(poolId ?? 0);
102
104
  const { rankings, refetch: refetchLB } = useTDLeaderboard(poolId ?? 0, { pollMs: poolId ? 30_000 : 0 });
103
- const { matches } = useTDMatches(matchId !== undefined ? { tournamentId: undefined } : undefined);
104
- const match = useMemo(() => matches.find((m) => m.id === matchId) ?? null, [matches, matchId]);
105
105
 
106
106
  // ── Config: real or fallback ─────────────────────────────────────────────
107
+ // matchProp is passed directly from MatchCalendar's onPoolPress — no extra fetch needed
107
108
  const config = useMemo(() => {
108
- if (poolId && pool && match) {
109
- try { return buildPMBConfig(pool, match); } catch { /* fall through */ }
109
+ if (poolId && pool && matchProp) {
110
+ try { return buildPMBConfig(pool, matchProp); } catch { /* fall through */ }
110
111
  }
111
112
  return poolId ? null : CSK_VS_RCB_CHALLENGE;
112
- }, [poolId, pool, match]);
113
+ }, [poolId, pool, matchProp]);
113
114
 
114
115
  const fallbackPool = poolId ? null : CSK_VS_RCB_POOL;
115
116
 
@@ -2,7 +2,7 @@
2
2
  "use client";
3
3
 
4
4
  import { useRef, useMemo, useEffect } from "react";
5
- import { motion } from "framer-motion";
5
+ import { motion, AnimatePresence } 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";
@@ -114,26 +114,38 @@ const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string; bg:
114
114
 
115
115
  function PoolPill({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) => void }) {
116
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
117
  const isClosed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
118
+ const isOpen = pool.status === TDPoolStatus.OPEN;
119
119
 
120
120
  return (
121
- <button
121
+ <motion.button
122
122
  onClick={() => onPress(pool)}
123
- className="flex items-center gap-1 px-1.5 py-0.5 rounded-md flex-shrink-0"
123
+ whileTap={{ scale: 0.93 }}
124
+ whileHover={{ scale: 1.04 }}
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"
124
127
  style={{
125
- background: style.bg,
126
- border: `1px solid ${style.color}20`,
127
- opacity: isClosed ? 0.5 : 1,
128
+ background: isOpen ? style.bg.replace("0.06", "0.10") : style.bg,
129
+ border: `1px solid ${style.color}${isOpen ? "50" : "25"}`,
130
+ opacity: isClosed ? 0.45 : 1,
131
+ boxShadow: isOpen ? `0 0 8px ${style.color}18` : undefined,
128
132
  }}
129
133
  >
130
- <span className="text-[7px] font-bold" style={{ ...OUTFIT, color: style.color }}>
134
+ <span className="text-[10px] font-bold" style={{ ...OUTFIT, color: "#fff" }}>
131
135
  {pool.display_price}
132
136
  </span>
133
137
  {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>
138
+ <span className="text-[8px] font-medium" style={{ ...OUTFIT, color: "rgba(255,255,255,0.65)" }}>
139
+ {pool.entry_count > 999 ? `${(pool.entry_count / 1000).toFixed(1)}k` : pool.entry_count}
140
+ </span>
141
+ )}
142
+ {isOpen && (
143
+ <span className="relative flex h-1.5 w-1.5 shrink-0">
144
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-60" style={{ backgroundColor: style.color }} />
145
+ <span className="relative inline-flex rounded-full h-1.5 w-1.5" style={{ backgroundColor: style.color }} />
146
+ </span>
135
147
  )}
136
- </button>
148
+ </motion.button>
137
149
  );
138
150
  }
139
151
 
@@ -164,21 +176,28 @@ function MatchGameSection({
164
176
  if (gameTypes.length === 0) return null;
165
177
 
166
178
  return (
167
- <div className="flex flex-col gap-1 mt-1 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
179
+ <div className="flex flex-col gap-1.5 mt-1.5 pt-2" style={{ borderTop: "1px solid rgba(255,255,255,0.08)" }}>
168
180
  {gameTypes.map((gt) => {
169
181
  const style = GAME_TYPE_STYLE[gt];
170
182
  const Icon = style.icon;
171
183
  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">
184
+ <div key={gt} className="flex flex-col gap-1">
185
+ <div className="flex items-center gap-1 px-0.5">
186
+ <Icon size={9} style={{ color: style.color }} />
187
+ <span className="text-[8px] barlowcondensedBold tracking-wide" style={{ color: "rgba(255,255,255,0.85)" }}>
176
188
  {style.label}
177
189
  </span>
178
190
  </div>
179
- <div className="flex gap-1 flex-wrap">
180
- {grouped[gt].map((pool) => (
181
- <PoolPill key={pool.id} pool={pool} onPress={onPoolPress ?? (() => {})} />
191
+ <div className="flex gap-1.5 flex-wrap">
192
+ {grouped[gt].map((pool, idx) => (
193
+ <motion.div
194
+ key={pool.id}
195
+ initial={{ opacity: 0, y: 6 }}
196
+ animate={{ opacity: 1, y: 0 }}
197
+ transition={{ delay: idx * 0.04, duration: 0.25, ease: "easeOut" }}
198
+ >
199
+ <PoolPill pool={pool} onPress={onPoolPress ?? (() => {})} />
200
+ </motion.div>
182
201
  ))}
183
202
  </div>
184
203
  </div>
@@ -190,7 +209,7 @@ function MatchGameSection({
190
209
 
191
210
  /* ── Match card with border shine animation ── */
192
211
 
193
- function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (pool: ITDPool) => void; partnerSource?: string }) {
212
+ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (pool: ITDPool, match: ITDMatch) => void; partnerSource?: string }) {
194
213
  const isLive = match.status === MATCH_STATUS.LIVE;
195
214
  const isCompleted = match.status === MATCH_STATUS.COMPLETED;
196
215
  const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
@@ -225,19 +244,23 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
225
244
  />
226
245
  )}
227
246
 
228
- <div
229
- className="relative flex flex-col gap-1.5 rounded-xl px-3 py-2.5"
247
+ <motion.div
248
+ className="relative flex flex-col gap-2 rounded-xl px-3.5 py-3"
230
249
  style={{
231
- background: isLive ? "rgba(10,10,18,0.95)" : "rgba(10,10,18,0.98)",
250
+ background: isLive
251
+ ? "linear-gradient(160deg, rgba(14,10,24,0.98) 0%, rgba(10,8,20,0.98) 100%)"
252
+ : "rgba(10,10,18,0.98)",
232
253
  }}
254
+ whileHover={{ backgroundColor: "rgba(18,18,28,0.98)" }}
255
+ transition={{ duration: 0.2 }}
233
256
  >
234
257
  {/* Time + live indicator */}
235
- <div className="flex items-center justify-between">
258
+ <div className="flex items-center justify-between gap-1">
236
259
  <span
237
- className="text-[10px]"
260
+ className="text-[11px] font-semibold"
238
261
  style={{
239
262
  ...OUTFIT,
240
- color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.35)" : "#fff",
263
+ color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "#fff",
241
264
  }}
242
265
  >
243
266
  {time}
@@ -254,26 +277,26 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
254
277
  </div>
255
278
  )}
256
279
  {isCompleted && match.winner && (
257
- <span className="text-[8px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.35)" }}>
280
+ <span className="text-[8px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.5)" }}>
258
281
  {match.winner.short_name || match.winner.name} won
259
282
  </span>
260
283
  )}
261
284
  </div>
262
285
 
263
286
  {/* Teams */}
264
- <div className="flex items-center gap-1.5 justify-center">
287
+ <div className="flex items-center gap-2 justify-center">
265
288
  <span
266
- className="barlowcondensedBold text-[20px] leading-none"
267
- style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
289
+ className="barlowcondensedBold text-[24px] leading-none tracking-wide"
290
+ style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
268
291
  >
269
292
  {getTeamShortName(match, "a")}
270
293
  </span>
271
- <span className="text-[9px]" style={{ color: "rgba(255,255,255,0.3)" }}>
272
- v
294
+ <span className="text-[10px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.5)" }}>
295
+ vs
273
296
  </span>
274
297
  <span
275
- className="barlowcondensedBold text-[20px] leading-none"
276
- style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
298
+ className="barlowcondensedBold text-[24px] leading-none tracking-wide"
299
+ style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
277
300
  >
278
301
  {getTeamShortName(match, "b")}
279
302
  </span>
@@ -282,10 +305,10 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
282
305
  {/* Venue */}
283
306
  {venue && (
284
307
  <div className="flex items-center gap-1 justify-center">
285
- <MapPin size={8} style={{ color: "rgba(255,255,255,0.3)" }} />
308
+ <MapPin size={9} style={{ color: "rgba(255,255,255,0.55)" }} />
286
309
  <span
287
- className="text-[8px] truncate max-w-[100px]"
288
- style={{ ...OUTFIT, color: "rgba(255,255,255,0.3)" }}
310
+ className="text-[9px] truncate max-w-[130px]"
311
+ style={{ ...OUTFIT, color: "rgba(255,255,255,0.7)" }}
289
312
  >
290
313
  {venue}
291
314
  </span>
@@ -298,9 +321,9 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
298
321
  {Array.from({ length: Math.min(Math.ceil(match.rating_popularity / 2), 5) }).map((_, i) => (
299
322
  <Zap
300
323
  key={i}
301
- size={7}
324
+ size={8}
302
325
  fill={isLive ? "#f83cc5" : "#22E3E8"}
303
- style={{ color: isLive ? "#f83cc5" : "#22E3E8", opacity: 0.4 + i * 0.15 }}
326
+ style={{ color: isLive ? "#f83cc5" : "#22E3E8", opacity: 0.45 + i * 0.13 }}
304
327
  />
305
328
  ))}
306
329
  </div>
@@ -310,17 +333,17 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
310
333
  <MatchGameSection
311
334
  matchId={match.id}
312
335
  isCompleted={isCompleted}
313
- onPoolPress={onPoolPress}
336
+ onPoolPress={onPoolPress ? (pool) => onPoolPress(pool, match) : undefined}
314
337
  partnerSource={partnerSource}
315
338
  />
316
- </div>
339
+ </motion.div>
317
340
  </div>
318
341
  );
319
342
  }
320
343
 
321
344
  /* ── Day column ── */
322
345
 
323
- function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay; index: number; onPoolPress?: (pool: ITDPool) => void; partnerSource?: string }) {
346
+ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay; index: number; onPoolPress?: (pool: ITDPool, match: ITDMatch) => void; partnerSource?: string }) {
324
347
  const hasToday = day.label === "TODAY";
325
348
  const hasLive = day.matches.some((m) => m.status === MATCH_STATUS.LIVE);
326
349
  const allCompleted = day.matches.every((m) => m.status === MATCH_STATUS.COMPLETED);
@@ -329,32 +352,32 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
329
352
  <motion.div
330
353
  initial={{ opacity: 0, y: 10 }}
331
354
  animate={{ opacity: 1, y: 0 }}
332
- transition={{ duration: 0.3, delay: index * 0.03 }}
333
- className="shrink-0 flex flex-col gap-1.5"
334
- style={{ scrollSnapAlign: "start", width: "140px" }}
355
+ transition={{ duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] }}
356
+ className="shrink-0 flex flex-col gap-2"
357
+ style={{ scrollSnapAlign: "start", width: "180px" }}
335
358
  >
336
359
  {/* Day header */}
337
- <div className="flex flex-col items-center gap-0.5 pb-1">
360
+ <div className="flex flex-col items-center gap-0.5 pb-1.5">
338
361
  <span
339
- className="text-[10px] barlowcondensedBold tracking-widest"
362
+ className="text-[11px] barlowcondensedBold tracking-widest"
340
363
  style={{
341
- color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.25)" : "#fff",
364
+ color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.9)",
342
365
  }}
343
366
  >
344
367
  {day.label}
345
368
  </span>
346
369
  <span
347
- className="text-[12px] font-semibold"
370
+ className="text-[13px] font-semibold"
348
371
  style={{
349
372
  ...OUTFIT,
350
- color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.25)" : "#fff",
373
+ color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "#fff",
351
374
  }}
352
375
  >
353
376
  {day.dayMonth}
354
377
  </span>
355
378
  {(hasToday || hasLive) && (
356
379
  <div
357
- className="w-5 h-[2px] rounded-full mt-0.5"
380
+ className="w-6 h-[2px] rounded-full mt-0.5"
358
381
  style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
359
382
  />
360
383
  )}
@@ -372,8 +395,8 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
372
395
 
373
396
  interface MatchCalendarProps {
374
397
  tournamentId?: number;
375
- /** Called when user taps a pool pill. Pass poolId + matchId to open the popup. */
376
- onPoolPress?: (pool: ITDPool) => void;
398
+ /** Called when user taps a pool pill. Receives both the pool and its parent match. */
399
+ onPoolPress?: (pool: ITDPool, match: ITDMatch) => void;
377
400
  /** Filter pools to a specific partner source (e.g. "iamgame") */
378
401
  partnerSource?: string;
379
402
  }
@@ -467,18 +490,18 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
467
490
  )}
468
491
 
469
492
  {/* Section header */}
470
- <div className="flex items-center gap-2 px-5 mb-2.5">
471
- <Calendar size={12} style={{ color: "#22E3E8" }} />
472
- <span className="barlowCondensedSemiBold text-[13px] tracking-wider text-white">
493
+ <div className="flex items-center gap-2 px-5 mb-3">
494
+ <Calendar size={13} style={{ color: "#22E3E8" }} />
495
+ <span className="barlowCondensedSemiBold text-[14px] tracking-wider text-white">
473
496
  SCHEDULE
474
497
  </span>
475
- <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
498
+ <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
476
499
  </div>
477
500
 
478
501
  {/* Scrollable day columns */}
479
502
  <div
480
503
  ref={scrollRef}
481
- className="flex gap-2.5 overflow-x-auto px-5 pb-3 scrollbar-hide"
504
+ className="flex gap-3 overflow-x-auto px-5 pb-4 scrollbar-hide"
482
505
  style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
483
506
  >
484
507
  {days.map((day, i) => (