@devrongx/games 0.4.37 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -2,7 +2,7 @@
2
2
  "use client";
3
3
 
4
4
  import { useRef, useMemo, useEffect } from "react";
5
- import { motion, AnimatePresence } from "framer-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 pills for a match ── */
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 PoolPill({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) => void }) {
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.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"
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.10") : style.bg,
129
- border: `1px solid ${style.color}${isOpen ? "50" : "25"}`,
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
- <span className="text-[10px] font-bold tracking-wide" style={{ ...OUTFIT, color: "#fff" }}>
141
- PLAY
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
- {pool.entry_count > 999 ? `${(pool.entry_count / 1000).toFixed(1)}k` : pool.entry_count} in
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-1.5 mt-1.5 pt-2" style={{ borderTop: "1px solid rgba(255,255,255,0.08)" }}>
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
- <div className="flex items-center gap-1 px-0.5">
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: "rgba(255,255,255,0.85)" }}>
210
+ <span className="text-[8px] barlowcondensedBold tracking-wide" style={{ color: style.color }}>
188
211
  {style.label}
189
212
  </span>
190
213
  </div>
191
- <div className="flex gap-1.5 flex-wrap">
192
- {grouped[gt].map((pool, idx) => (
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: 6 }}
219
+ initial={{ opacity: 0, y: 4 }}
196
220
  animate={{ opacity: 1, y: 0 }}
197
- transition={{ delay: idx * 0.04, duration: 0.25, ease: "easeOut" }}
221
+ transition={{ delay: idx * 0.03, duration: 0.2, ease: "easeOut" }}
198
222
  >
199
- <PoolPill pool={pool} onPress={onPoolPress ?? (() => {})} />
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-2 rounded-xl px-3.5 py-3"
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-[11px] font-semibold"
284
+ className="text-[10px] font-semibold"
261
285
  style={{
262
286
  ...OUTFIT,
263
- color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "#fff",
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-[24px] leading-none tracking-wide"
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-[10px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.5)" }}>
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-[24px] leading-none tracking-wide"
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={9} style={{ color: "rgba(255,255,255,0.55)" }} />
332
+ <MapPin size={8} style={{ color: "rgba(255,255,255,0.45)" }} />
309
333
  <span
310
- className="text-[9px] truncate max-w-[130px]"
311
- style={{ ...OUTFIT, color: "rgba(255,255,255,0.7)" }}
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: "180px" }}
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.5">
370
+ <div className="flex flex-col items-center gap-0.5 pb-1">
361
371
  <span
362
- className="text-[11px] barlowcondensedBold tracking-widest"
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-[13px] font-semibold"
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-6 h-[2px] rounded-full mt-0.5"
390
+ className="w-5 h-[2px] rounded-full mt-0.5"
381
391
  style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
382
392
  />
383
393
  )}