@devrongx/games 0.4.37 → 0.4.39

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.39",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
@@ -2,28 +2,20 @@
2
2
  "use client";
3
3
 
4
4
  import { useRef, useMemo, useEffect } from "react";
5
- import { motion, AnimatePresence } from "framer-motion";
6
- import { Calendar, MapPin, Loader2, Trophy, Zap, Target, Users } from "lucide-react";
5
+ import { motion } from "framer-motion";
6
+ import { Calendar, MapPin, Loader2, Trophy, Target, Users, Zap } from "lucide-react";
7
7
  import { useTDMatches } from "./useTDMatches";
8
8
  import { useTDPools } from "../pools/hooks";
9
9
  import type { ITDPool } from "../pools/types";
10
10
  import { TDGameType, TDPoolStatus } from "../pools/types";
11
11
  import type { ITDMatch } from "./types";
12
12
 
13
- // TD match status enum values
14
- const MATCH_STATUS = {
15
- UPCOMING: 1,
16
- LIVE: 2,
17
- COMPLETED: 5,
18
- } as const;
19
-
13
+ const MATCH_STATUS = { UPCOMING: 1, LIVE: 2, COMPLETED: 5 } as const;
20
14
  const OUTFIT = { fontFamily: "Outfit, sans-serif" };
21
15
 
22
16
  /* ── Date helpers ── */
23
17
 
24
- function toLocalDate(iso: string): Date {
25
- return new Date(iso);
26
- }
18
+ function toLocalDate(iso: string): Date { return new Date(iso); }
27
19
 
28
20
  function formatDayMonth(d: Date): string {
29
21
  return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`;
@@ -41,9 +33,7 @@ function isSameDay(a: Date, b: Date): boolean {
41
33
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
42
34
  }
43
35
 
44
- function isToday(d: Date): boolean {
45
- return isSameDay(d, new Date());
46
- }
36
+ function isToday(d: Date): boolean { return isSameDay(d, new Date()); }
47
37
 
48
38
  function isTomorrow(d: Date): boolean {
49
39
  const t = new Date();
@@ -62,12 +52,21 @@ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
62
52
  return t.short_name || t.name.slice(0, 3).toUpperCase();
63
53
  }
64
54
 
65
- /** Map popularity 0-10 to border shine opacity (0 = no shine, 10 = full intensity) */
66
55
  function getShineOpacity(popularity: number): number {
67
56
  if (popularity <= 0) return 0;
68
57
  return Math.min(popularity / 10, 1);
69
58
  }
70
59
 
60
+ function humanizeCount(n: number): string {
61
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
62
+ return String(n);
63
+ }
64
+
65
+ function isFreePool(pool: ITDPool): boolean {
66
+ const dp = (pool.display_price || "").toLowerCase().trim();
67
+ return dp === "" || dp === "free" || (pool.entry_fee === 0 && !dp);
68
+ }
69
+
71
70
  /* ── Grouping: by date ── */
72
71
 
73
72
  interface IMatchDay {
@@ -80,69 +79,58 @@ interface IMatchDay {
80
79
 
81
80
  function groupByDate(matches: ITDMatch[]): IMatchDay[] {
82
81
  const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
83
-
84
82
  for (const m of matches) {
85
83
  if (!m.scheduled_start_at) continue;
86
84
  const d = toLocalDate(m.scheduled_start_at);
87
85
  const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
88
86
  const existing = map.get(key);
89
- if (existing) {
90
- existing.matches.push(m);
91
- } else {
92
- map.set(key, { date: d, matches: [m] });
93
- }
87
+ if (existing) { existing.matches.push(m); }
88
+ else { map.set(key, { date: d, matches: [m] }); }
94
89
  }
95
-
96
90
  return Array.from(map.entries())
97
91
  .sort(([a], [b]) => a.localeCompare(b))
98
92
  .map(([dateKey, { date, matches: dayMatches }]) => ({
99
- dateKey,
100
- date,
101
- label: getDayLabel(date),
102
- dayMonth: formatDayMonth(date),
103
- matches: dayMatches,
93
+ dateKey, date, label: getDayLabel(date), dayMonth: formatDayMonth(date), matches: dayMatches,
104
94
  }));
105
95
  }
106
96
 
107
- /* ── Pool pills for a match ── */
97
+ /* ── Pool display ── */
108
98
 
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" },
99
+ const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string; label: string }> = {
100
+ [TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8", label: "Pre-Match Bets" },
101
+ [TDGameType.FANTASY_11]: { icon: Users, color: "#9945FF", label: "Fantasy 11" },
102
+ [TDGameType.BALL_SEQUENCE]: { icon: Zap, color: "#FF9945", label: "Ball Sequence" },
113
103
  };
114
104
 
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)", label: "Play" };
105
+ /** Tiny inline pill for a single pool */
106
+ function PoolChip({ pool, color, onPress }: { pool: ITDPool; color: string; onPress: (p: ITDPool) => void }) {
117
107
  const isClosed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
118
108
  const isOpen = pool.status === TDPoolStatus.OPEN;
109
+ const free = isFreePool(pool);
119
110
 
120
111
  return (
121
112
  <motion.button
122
113
  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"
114
+ whileTap={{ scale: 0.92 }}
115
+ className="flex items-center gap-1 px-2 py-1 rounded-md shrink-0"
127
116
  style={{
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,
117
+ background: `${color}${isOpen ? "14" : "0a"}`,
118
+ border: `1px solid ${color}${isOpen ? "40" : "20"}`,
119
+ opacity: isClosed ? 0.4 : 1,
132
120
  }}
133
121
  >
134
122
  {isOpen && (
135
- <span className="relative flex h-1.5 w-1.5 shrink-0">
136
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-60" style={{ backgroundColor: style.color }} />
137
- <span className="relative inline-flex rounded-full h-1.5 w-1.5" style={{ backgroundColor: style.color }} />
123
+ <span className="relative flex h-1 w-1 shrink-0">
124
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-50" style={{ backgroundColor: color }} />
125
+ <span className="relative inline-flex rounded-full h-1 w-1" style={{ backgroundColor: color }} />
138
126
  </span>
139
127
  )}
140
- <span className="text-[10px] font-bold tracking-wide" style={{ ...OUTFIT, color: "#fff" }}>
141
- PLAY
128
+ <span className="text-[9px] font-bold" style={{ ...OUTFIT, color: free ? "rgba(255,255,255,0.8)" : "#fff" }}>
129
+ {free ? "Free" : pool.display_price}
142
130
  </span>
143
131
  {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
132
+ <span className="text-[7px]" style={{ ...OUTFIT, color: `${color}90` }}>
133
+ {humanizeCount(pool.entry_count)}
146
134
  </span>
147
135
  )}
148
136
  </motion.button>
@@ -150,10 +138,7 @@ function PoolPill({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) =
150
138
  }
151
139
 
152
140
  function MatchGameSection({
153
- matchId,
154
- isCompleted,
155
- onPoolPress,
156
- partnerSource,
141
+ matchId, isCompleted, onPoolPress, partnerSource,
157
142
  }: {
158
143
  matchId: number;
159
144
  isCompleted: boolean;
@@ -161,14 +146,10 @@ function MatchGameSection({
161
146
  partnerSource?: string;
162
147
  }) {
163
148
  const { pools, loading } = useTDPools(matchId, partnerSource);
164
-
165
149
  if (isCompleted || loading || pools.length === 0) return null;
166
150
 
167
- // Group by game_type (integer)
168
151
  const grouped: Record<number, ITDPool[]> = {};
169
- for (const p of pools) {
170
- (grouped[p.game_type] ??= []).push(p);
171
- }
152
+ for (const p of pools) { (grouped[p.game_type] ??= []).push(p); }
172
153
 
173
154
  const gameTypes = (Object.keys(grouped) as unknown as number[])
174
155
  .map(Number)
@@ -176,28 +157,37 @@ function MatchGameSection({
176
157
  if (gameTypes.length === 0) return null;
177
158
 
178
159
  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)" }}>
160
+ <div className="flex flex-col gap-1.5 mt-1.5 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.07)" }}>
180
161
  {gameTypes.map((gt) => {
181
162
  const style = GAME_TYPE_STYLE[gt];
182
163
  const Icon = style.icon;
164
+ // Sort: free first, then by display_price
165
+ const sorted = [...grouped[gt]].sort((a, b) => {
166
+ const af = isFreePool(a) ? 0 : 1;
167
+ const bf = isFreePool(b) ? 0 : 1;
168
+ if (af !== bf) return af - bf;
169
+ return a.entry_fee - b.entry_fee;
170
+ });
171
+ // Deduplicate: keep only one free pool per game type
172
+ const seen = new Set<string>();
173
+ const deduped = sorted.filter((p) => {
174
+ const key = isFreePool(p) ? "free" : p.display_price;
175
+ if (seen.has(key)) return false;
176
+ seen.add(key);
177
+ return true;
178
+ });
179
+
183
180
  return (
184
181
  <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)" }}>
182
+ <div className="flex items-center gap-1">
183
+ <Icon size={8} style={{ color: style.color }} />
184
+ <span className="text-[7px] barlowcondensedBold tracking-wider uppercase" style={{ color: `${style.color}cc` }}>
188
185
  {style.label}
189
186
  </span>
190
187
  </div>
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>
188
+ <div className="flex gap-1 flex-wrap">
189
+ {deduped.map((pool) => (
190
+ <PoolChip key={pool.id} pool={pool} color={style.color} onPress={onPoolPress ?? (() => {})} />
201
191
  ))}
202
192
  </div>
203
193
  </div>
@@ -207,7 +197,7 @@ function MatchGameSection({
207
197
  );
208
198
  }
209
199
 
210
- /* ── Match card with border shine animation ── */
200
+ /* ── Match card ── */
211
201
 
212
202
  function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (pool: ITDPool, match: ITDMatch) => void; partnerSource?: string }) {
213
203
  const isLive = match.status === MATCH_STATUS.LIVE;
@@ -219,7 +209,6 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
219
209
 
220
210
  return (
221
211
  <div className="relative w-full rounded-xl p-[1px] overflow-hidden">
222
- {/* Spinning border shine — intensity based on popularity */}
223
212
  {showShine && (
224
213
  <div
225
214
  className="absolute inset-[-50%] pointer-events-none"
@@ -231,38 +220,21 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
231
220
  }}
232
221
  />
233
222
  )}
234
-
235
- {/* Static border for no-shine / completed */}
236
223
  {!showShine && (
237
- <div
238
- className="absolute inset-0 rounded-xl"
239
- style={{
240
- border: isCompleted
241
- ? "1px solid rgba(255,255,255,0.06)"
242
- : "1px solid rgba(255,255,255,0.08)",
243
- }}
244
- />
224
+ <div className="absolute inset-0 rounded-xl" style={{ border: isCompleted ? "1px solid rgba(255,255,255,0.06)" : "1px solid rgba(255,255,255,0.08)" }} />
245
225
  )}
246
226
 
247
227
  <motion.div
248
- className="relative flex flex-col gap-2 rounded-xl px-3.5 py-3"
228
+ className="relative flex flex-col gap-1 rounded-xl px-2.5 py-2"
249
229
  style={{
250
230
  background: isLive
251
231
  ? "linear-gradient(160deg, rgba(14,10,24,0.98) 0%, rgba(10,8,20,0.98) 100%)"
252
232
  : "rgba(10,10,18,0.98)",
253
233
  }}
254
- whileHover={{ backgroundColor: "rgba(18,18,28,0.98)" }}
255
- transition={{ duration: 0.2 }}
256
234
  >
257
- {/* Time + live indicator */}
258
- <div className="flex items-center justify-between gap-1">
259
- <span
260
- className="text-[11px] font-semibold"
261
- style={{
262
- ...OUTFIT,
263
- color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "#fff",
264
- }}
265
- >
235
+ {/* Time row */}
236
+ <div className="flex items-center justify-between">
237
+ <span className="text-[9px] font-semibold" style={{ ...OUTFIT, color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.4)" : "rgba(255,255,255,0.6)" }}>
266
238
  {time}
267
239
  </span>
268
240
  {isLive && (
@@ -271,65 +243,38 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
271
243
  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
272
244
  <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
273
245
  </span>
274
- <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
275
- LIVE
276
- </span>
246
+ <span className="text-[7px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>LIVE</span>
277
247
  </div>
278
248
  )}
279
249
  {isCompleted && match.winner && (
280
- <span className="text-[8px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.5)" }}>
250
+ <span className="text-[7px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.45)" }}>
281
251
  {match.winner.short_name || match.winner.name} won
282
252
  </span>
283
253
  )}
284
254
  </div>
285
255
 
286
256
  {/* Teams */}
287
- <div className="flex items-center gap-2 justify-center">
288
- <span
289
- className="barlowcondensedBold text-[24px] leading-none tracking-wide"
290
- style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
291
- >
257
+ <div className="flex items-center gap-1.5 justify-center">
258
+ <span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}>
292
259
  {getTeamShortName(match, "a")}
293
260
  </span>
294
- <span className="text-[10px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.5)" }}>
295
- vs
296
- </span>
297
- <span
298
- className="barlowcondensedBold text-[24px] leading-none tracking-wide"
299
- style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
300
- >
261
+ <span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.4)" }}>vs</span>
262
+ <span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}>
301
263
  {getTeamShortName(match, "b")}
302
264
  </span>
303
265
  </div>
304
266
 
305
267
  {/* Venue */}
306
268
  {venue && (
307
- <div className="flex items-center gap-1 justify-center">
308
- <MapPin size={9} style={{ color: "rgba(255,255,255,0.55)" }} />
309
- <span
310
- className="text-[9px] truncate max-w-[130px]"
311
- style={{ ...OUTFIT, color: "rgba(255,255,255,0.7)" }}
312
- >
269
+ <div className="flex items-center gap-0.5 justify-center">
270
+ <MapPin size={7} style={{ color: "rgba(255,255,255,0.4)" }} />
271
+ <span className="text-[7px] truncate max-w-[110px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.55)" }}>
313
272
  {venue}
314
273
  </span>
315
274
  </div>
316
275
  )}
317
276
 
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
- {/* Game modes — driven by real pools */}
277
+ {/* Pools */}
333
278
  <MatchGameSection
334
279
  matchId={match.id}
335
280
  isCompleted={isCompleted}
@@ -353,37 +298,26 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
353
298
  initial={{ opacity: 0, y: 10 }}
354
299
  animate={{ opacity: 1, y: 0 }}
355
300
  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" }}
301
+ className="shrink-0 flex flex-col gap-1.5"
302
+ style={{ scrollSnapAlign: "start", width: "150px" }}
358
303
  >
359
- {/* Day header */}
360
- <div className="flex flex-col items-center gap-0.5 pb-1.5">
304
+ <div className="flex flex-col items-center gap-0.5 pb-1">
361
305
  <span
362
- className="text-[11px] barlowcondensedBold tracking-widest"
363
- style={{
364
- color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.9)",
365
- }}
306
+ className="text-[9px] barlowcondensedBold tracking-widest"
307
+ style={{ color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.3)" : "rgba(255,255,255,0.85)" }}
366
308
  >
367
309
  {day.label}
368
310
  </span>
369
311
  <span
370
- className="text-[13px] font-semibold"
371
- style={{
372
- ...OUTFIT,
373
- color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "#fff",
374
- }}
312
+ className="text-[11px] font-semibold"
313
+ style={{ ...OUTFIT, color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
375
314
  >
376
315
  {day.dayMonth}
377
316
  </span>
378
317
  {(hasToday || hasLive) && (
379
- <div
380
- className="w-6 h-[2px] rounded-full mt-0.5"
381
- style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
382
- />
318
+ <div className="w-4 h-[1.5px] rounded-full mt-0.5" style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }} />
383
319
  )}
384
320
  </div>
385
-
386
- {/* Match cards for the day */}
387
321
  {day.matches.map((match) => (
388
322
  <MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
389
323
  ))}
@@ -391,26 +325,19 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
391
325
  );
392
326
  }
393
327
 
394
- /* ── Main calendar component ── */
328
+ /* ── Main calendar ── */
395
329
 
396
330
  interface MatchCalendarProps {
397
331
  tournamentId?: number;
398
- /** Called when user taps a pool pill. Receives both the pool and its parent match. */
399
332
  onPoolPress?: (pool: ITDPool, match: ITDMatch) => void;
400
- /** Filter pools to a specific partner source (e.g. "iamgame") */
401
333
  partnerSource?: string;
402
334
  }
403
335
 
404
336
  export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
405
337
  const scrollRef = useRef<HTMLDivElement>(null);
406
- const { matches, isLoading, error } = useTDMatches({
407
- tournamentId,
408
- limit: 100,
409
- });
410
-
338
+ const { matches, isLoading, error } = useTDMatches({ tournamentId, limit: 100 });
411
339
  const days = useMemo(() => groupByDate(matches), [matches]);
412
340
 
413
- // Auto-scroll to today or first upcoming day
414
341
  const scrollTargetIndex = useMemo(() => {
415
342
  const tIdx = days.findIndex((d) => isToday(d.date));
416
343
  if (tIdx >= 0) return tIdx;
@@ -422,11 +349,8 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
422
349
 
423
350
  useEffect(() => {
424
351
  if (scrollRef.current && scrollTargetIndex > 0) {
425
- const container = scrollRef.current;
426
- const target = container.children[scrollTargetIndex] as HTMLElement;
427
- if (target) {
428
- container.scrollTo({ left: target.offsetLeft - 20, behavior: "smooth" });
429
- }
352
+ const target = scrollRef.current.children[scrollTargetIndex] as HTMLElement;
353
+ if (target) scrollRef.current.scrollTo({ left: target.offsetLeft - 16, behavior: "smooth" });
430
354
  }
431
355
  }, [scrollTargetIndex, days]);
432
356
 
@@ -443,42 +367,33 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
443
367
  if (error || matches.length === 0) return null;
444
368
 
445
369
  return (
446
- <div className="relative z-30 w-full mt-6 mb-4">
447
- {/* Tournament info header */}
370
+ <div className="relative z-30 w-full mt-5 mb-3">
448
371
  {tournament && (
449
- <div className="px-5 mb-3">
450
- <div className="flex items-center gap-2 mb-1">
451
- <Trophy size={14} style={{ color: "#22E3E8" }} />
452
- <span className="barlowcondensedBold text-[16px] tracking-wide text-white">
453
- {tournament.name}
454
- </span>
372
+ <div className="px-5 mb-2.5">
373
+ <div className="flex items-center gap-2 mb-0.5">
374
+ <Trophy size={13} style={{ color: "#22E3E8" }} />
375
+ <span className="barlowcondensedBold text-[15px] tracking-wide text-white">{tournament.name}</span>
455
376
  </div>
456
377
  <div className="flex items-center gap-3">
457
378
  {tournament.start_date && tournament.end_date && (
458
- <span className="text-[11px] text-white" style={OUTFIT}>
379
+ <span className="text-[10px] text-white/50" style={OUTFIT}>
459
380
  {formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
460
381
  </span>
461
382
  )}
462
- <span className="text-[11px] text-white" style={OUTFIT}>
463
- {matches.length} matches announced
464
- </span>
383
+ <span className="text-[10px] text-white/50" style={OUTFIT}>{matches.length} matches</span>
465
384
  </div>
466
385
  </div>
467
386
  )}
468
387
 
469
- {/* Section header */}
470
- <div className="flex items-center gap-2 px-5 mb-3">
471
- <Calendar size={13} style={{ color: "#22E3E8" }} />
472
- <span className="barlowCondensedSemiBold text-[14px] tracking-wider text-white">
473
- SCHEDULE
474
- </span>
475
- <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
388
+ <div className="flex items-center gap-2 px-5 mb-2.5">
389
+ <Calendar size={12} style={{ color: "#22E3E8" }} />
390
+ <span className="barlowCondensedSemiBold text-[13px] tracking-wider text-white">SCHEDULE</span>
391
+ <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.07)" }} />
476
392
  </div>
477
393
 
478
- {/* Scrollable day columns */}
479
394
  <div
480
395
  ref={scrollRef}
481
- className="flex gap-3 overflow-x-auto px-5 pb-4 scrollbar-hide"
396
+ className="flex gap-2.5 overflow-x-auto px-5 pb-3 scrollbar-hide"
482
397
  style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
483
398
  >
484
399
  {days.map((day, i) => (