@devrongx/games 0.4.38 → 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.38",
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",
@@ -3,27 +3,19 @@
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 } from "lucide-react";
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,7 +52,6 @@ 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);
@@ -73,10 +62,9 @@ function humanizeCount(n: number): string {
73
62
  return String(n);
74
63
  }
75
64
 
76
- /** Determine if a pool is free based on display_price and entry_fee */
77
65
  function isFreePool(pool: ITDPool): boolean {
78
66
  const dp = (pool.display_price || "").toLowerCase().trim();
79
- return dp === "" || dp === "free" || (pool.entry_fee === 0 && dp === "");
67
+ return dp === "" || dp === "free" || (pool.entry_fee === 0 && !dp);
80
68
  }
81
69
 
82
70
  /* ── Grouping: by date ── */
@@ -91,40 +79,31 @@ interface IMatchDay {
91
79
 
92
80
  function groupByDate(matches: ITDMatch[]): IMatchDay[] {
93
81
  const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
94
-
95
82
  for (const m of matches) {
96
83
  if (!m.scheduled_start_at) continue;
97
84
  const d = toLocalDate(m.scheduled_start_at);
98
85
  const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
99
86
  const existing = map.get(key);
100
- if (existing) {
101
- existing.matches.push(m);
102
- } else {
103
- map.set(key, { date: d, matches: [m] });
104
- }
87
+ if (existing) { existing.matches.push(m); }
88
+ else { map.set(key, { date: d, matches: [m] }); }
105
89
  }
106
-
107
90
  return Array.from(map.entries())
108
91
  .sort(([a], [b]) => a.localeCompare(b))
109
92
  .map(([dateKey, { date, matches: dayMatches }]) => ({
110
- dateKey,
111
- date,
112
- label: getDayLabel(date),
113
- dayMonth: formatDayMonth(date),
114
- matches: dayMatches,
93
+ dateKey, date, label: getDayLabel(date), dayMonth: formatDayMonth(date), matches: dayMatches,
115
94
  }));
116
95
  }
117
96
 
118
- /* ── Pool row item ── */
97
+ /* ── Pool display ── */
119
98
 
120
- const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string; bg: string; label: string }> = {
121
- [TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8", bg: "rgba(34,227,232,0.06)", label: "Pre-Match Bets" },
122
- [TDGameType.FANTASY_11]: { icon: Users, color: "#9945FF", bg: "rgba(153,69,255,0.06)", label: "Fantasy 11" },
123
- [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" },
124
103
  };
125
104
 
126
- function PoolRow({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) => void }) {
127
- 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 }) {
128
107
  const isClosed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
129
108
  const isOpen = pool.status === TDPoolStatus.OPEN;
130
109
  const free = isFreePool(pool);
@@ -132,31 +111,26 @@ function PoolRow({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) =>
132
111
  return (
133
112
  <motion.button
134
113
  onClick={() => onPress(pool)}
135
- whileTap={{ scale: 0.96 }}
136
- className="flex items-center gap-1.5 w-full px-2 py-1.5 rounded-lg"
114
+ whileTap={{ scale: 0.92 }}
115
+ className="flex items-center gap-1 px-2 py-1 rounded-md shrink-0"
137
116
  style={{
138
- background: isOpen ? style.bg.replace("0.06", "0.08") : style.bg,
139
- border: `1px solid ${style.color}${isOpen ? "30" : "18"}`,
140
- opacity: isClosed ? 0.45 : 1,
117
+ background: `${color}${isOpen ? "14" : "0a"}`,
118
+ border: `1px solid ${color}${isOpen ? "40" : "20"}`,
119
+ opacity: isClosed ? 0.4 : 1,
141
120
  }}
142
121
  >
143
- {/* Live dot */}
144
122
  {isOpen && (
145
- <span className="relative flex h-1.5 w-1.5 shrink-0">
146
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-60" style={{ backgroundColor: style.color }} />
147
- <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 }} />
148
126
  </span>
149
127
  )}
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}
128
+ <span className="text-[9px] font-bold" style={{ ...OUTFIT, color: free ? "rgba(255,255,255,0.8)" : "#fff" }}>
129
+ {free ? "Free" : pool.display_price}
154
130
  </span>
155
-
156
- {/* Entry count for non-free pools, or always if there are entries */}
157
131
  {pool.entry_count > 0 && (
158
- <span className="text-[8px] font-medium shrink-0" style={{ ...OUTFIT, color: `${style.color}99` }}>
159
- {humanizeCount(pool.entry_count)} in
132
+ <span className="text-[7px]" style={{ ...OUTFIT, color: `${color}90` }}>
133
+ {humanizeCount(pool.entry_count)}
160
134
  </span>
161
135
  )}
162
136
  </motion.button>
@@ -164,10 +138,7 @@ function PoolRow({ pool, onPress }: { pool: ITDPool; onPress: (pool: ITDPool) =>
164
138
  }
165
139
 
166
140
  function MatchGameSection({
167
- matchId,
168
- isCompleted,
169
- onPoolPress,
170
- partnerSource,
141
+ matchId, isCompleted, onPoolPress, partnerSource,
171
142
  }: {
172
143
  matchId: number;
173
144
  isCompleted: boolean;
@@ -175,14 +146,10 @@ function MatchGameSection({
175
146
  partnerSource?: string;
176
147
  }) {
177
148
  const { pools, loading } = useTDPools(matchId, partnerSource);
178
-
179
149
  if (isCompleted || loading || pools.length === 0) return null;
180
150
 
181
- // Group by game_type (integer)
182
151
  const grouped: Record<number, ITDPool[]> = {};
183
- for (const p of pools) {
184
- (grouped[p.game_type] ??= []).push(p);
185
- }
152
+ for (const p of pools) { (grouped[p.game_type] ??= []).push(p); }
186
153
 
187
154
  const gameTypes = (Object.keys(grouped) as unknown as number[])
188
155
  .map(Number)
@@ -190,38 +157,37 @@ function MatchGameSection({
190
157
  if (gameTypes.length === 0) return null;
191
158
 
192
159
  return (
193
- <div className="flex flex-col gap-2 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)" }}>
194
161
  {gameTypes.map((gt) => {
195
162
  const style = GAME_TYPE_STYLE[gt];
196
163
  const Icon = style.icon;
197
- // Sort: free pools first, then by display_price ascending
164
+ // Sort: free first, then by display_price
198
165
  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;
166
+ const af = isFreePool(a) ? 0 : 1;
167
+ const bf = isFreePool(b) ? 0 : 1;
168
+ if (af !== bf) return af - bf;
202
169
  return a.entry_fee - b.entry_fee;
203
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
+ });
204
179
 
205
180
  return (
206
181
  <div key={gt} className="flex flex-col gap-1">
207
- {/* Game type pill header */}
208
- <div className="flex items-center gap-1 px-0.5 mb-0.5">
209
- <Icon size={9} style={{ color: style.color }} />
210
- <span className="text-[8px] barlowcondensedBold tracking-wide" style={{ color: style.color }}>
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` }}>
211
185
  {style.label}
212
186
  </span>
213
187
  </div>
214
- {/* Pool rows */}
215
- <div className="flex flex-col gap-1">
216
- {sorted.map((pool, idx) => (
217
- <motion.div
218
- key={pool.id}
219
- initial={{ opacity: 0, y: 4 }}
220
- animate={{ opacity: 1, y: 0 }}
221
- transition={{ delay: idx * 0.03, duration: 0.2, ease: "easeOut" }}
222
- >
223
- <PoolRow pool={pool} onPress={onPoolPress ?? (() => {})} />
224
- </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 ?? (() => {})} />
225
191
  ))}
226
192
  </div>
227
193
  </div>
@@ -231,7 +197,7 @@ function MatchGameSection({
231
197
  );
232
198
  }
233
199
 
234
- /* ── Match card with border shine animation ── */
200
+ /* ── Match card ── */
235
201
 
236
202
  function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (pool: ITDPool, match: ITDMatch) => void; partnerSource?: string }) {
237
203
  const isLive = match.status === MATCH_STATUS.LIVE;
@@ -243,7 +209,6 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
243
209
 
244
210
  return (
245
211
  <div className="relative w-full rounded-xl p-[1px] overflow-hidden">
246
- {/* Spinning border shine — intensity based on popularity */}
247
212
  {showShine && (
248
213
  <div
249
214
  className="absolute inset-[-50%] pointer-events-none"
@@ -255,38 +220,21 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
255
220
  }}
256
221
  />
257
222
  )}
258
-
259
- {/* Static border for no-shine / completed */}
260
223
  {!showShine && (
261
- <div
262
- className="absolute inset-0 rounded-xl"
263
- style={{
264
- border: isCompleted
265
- ? "1px solid rgba(255,255,255,0.06)"
266
- : "1px solid rgba(255,255,255,0.08)",
267
- }}
268
- />
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)" }} />
269
225
  )}
270
226
 
271
227
  <motion.div
272
- className="relative flex flex-col gap-1.5 rounded-xl px-3 py-2.5"
228
+ className="relative flex flex-col gap-1 rounded-xl px-2.5 py-2"
273
229
  style={{
274
230
  background: isLive
275
231
  ? "linear-gradient(160deg, rgba(14,10,24,0.98) 0%, rgba(10,8,20,0.98) 100%)"
276
232
  : "rgba(10,10,18,0.98)",
277
233
  }}
278
- whileHover={{ backgroundColor: "rgba(18,18,28,0.98)" }}
279
- transition={{ duration: 0.2 }}
280
234
  >
281
- {/* Time + live indicator */}
282
- <div className="flex items-center justify-between gap-1">
283
- <span
284
- className="text-[10px] font-semibold"
285
- style={{
286
- ...OUTFIT,
287
- color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "rgba(255,255,255,0.7)",
288
- }}
289
- >
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)" }}>
290
238
  {time}
291
239
  </span>
292
240
  {isLive && (
@@ -295,51 +243,38 @@ function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onP
295
243
  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
296
244
  <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
297
245
  </span>
298
- <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
299
- LIVE
300
- </span>
246
+ <span className="text-[7px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>LIVE</span>
301
247
  </div>
302
248
  )}
303
249
  {isCompleted && match.winner && (
304
- <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)" }}>
305
251
  {match.winner.short_name || match.winner.name} won
306
252
  </span>
307
253
  )}
308
254
  </div>
309
255
 
310
256
  {/* Teams */}
311
- <div className="flex items-center gap-2 justify-center">
312
- <span
313
- className="barlowcondensedBold text-[20px] leading-none tracking-wide"
314
- style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
315
- >
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" }}>
316
259
  {getTeamShortName(match, "a")}
317
260
  </span>
318
- <span className="text-[9px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.45)" }}>
319
- vs
320
- </span>
321
- <span
322
- className="barlowcondensedBold text-[20px] leading-none tracking-wide"
323
- style={{ color: isCompleted ? "rgba(255,255,255,0.4)" : "#fff" }}
324
- >
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" }}>
325
263
  {getTeamShortName(match, "b")}
326
264
  </span>
327
265
  </div>
328
266
 
329
267
  {/* Venue */}
330
268
  {venue && (
331
- <div className="flex items-center gap-1 justify-center">
332
- <MapPin size={8} style={{ color: "rgba(255,255,255,0.45)" }} />
333
- <span
334
- className="text-[8px] truncate max-w-[120px]"
335
- style={{ ...OUTFIT, color: "rgba(255,255,255,0.6)" }}
336
- >
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)" }}>
337
272
  {venue}
338
273
  </span>
339
274
  </div>
340
275
  )}
341
276
 
342
- {/* Game modes — driven by real pools */}
277
+ {/* Pools */}
343
278
  <MatchGameSection
344
279
  matchId={match.id}
345
280
  isCompleted={isCompleted}
@@ -363,37 +298,26 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
363
298
  initial={{ opacity: 0, y: 10 }}
364
299
  animate={{ opacity: 1, y: 0 }}
365
300
  transition={{ duration: 0.35, delay: index * 0.04, ease: [0.25, 0.46, 0.45, 0.94] }}
366
- className="shrink-0 flex flex-col gap-2"
367
- style={{ scrollSnapAlign: "start", width: "160px" }}
301
+ className="shrink-0 flex flex-col gap-1.5"
302
+ style={{ scrollSnapAlign: "start", width: "150px" }}
368
303
  >
369
- {/* Day header */}
370
304
  <div className="flex flex-col items-center gap-0.5 pb-1">
371
305
  <span
372
- className="text-[10px] barlowcondensedBold tracking-widest"
373
- style={{
374
- color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.9)",
375
- }}
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)" }}
376
308
  >
377
309
  {day.label}
378
310
  </span>
379
311
  <span
380
- className="text-[12px] font-semibold"
381
- style={{
382
- ...OUTFIT,
383
- color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "#fff",
384
- }}
312
+ className="text-[11px] font-semibold"
313
+ style={{ ...OUTFIT, color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
385
314
  >
386
315
  {day.dayMonth}
387
316
  </span>
388
317
  {(hasToday || hasLive) && (
389
- <div
390
- className="w-5 h-[2px] rounded-full mt-0.5"
391
- style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
392
- />
318
+ <div className="w-4 h-[1.5px] rounded-full mt-0.5" style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }} />
393
319
  )}
394
320
  </div>
395
-
396
- {/* Match cards for the day */}
397
321
  {day.matches.map((match) => (
398
322
  <MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
399
323
  ))}
@@ -401,26 +325,19 @@ function DayColumn({ day, index, onPoolPress, partnerSource }: { day: IMatchDay;
401
325
  );
402
326
  }
403
327
 
404
- /* ── Main calendar component ── */
328
+ /* ── Main calendar ── */
405
329
 
406
330
  interface MatchCalendarProps {
407
331
  tournamentId?: number;
408
- /** Called when user taps a pool pill. Receives both the pool and its parent match. */
409
332
  onPoolPress?: (pool: ITDPool, match: ITDMatch) => void;
410
- /** Filter pools to a specific partner source (e.g. "iamgame") */
411
333
  partnerSource?: string;
412
334
  }
413
335
 
414
336
  export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
415
337
  const scrollRef = useRef<HTMLDivElement>(null);
416
- const { matches, isLoading, error } = useTDMatches({
417
- tournamentId,
418
- limit: 100,
419
- });
420
-
338
+ const { matches, isLoading, error } = useTDMatches({ tournamentId, limit: 100 });
421
339
  const days = useMemo(() => groupByDate(matches), [matches]);
422
340
 
423
- // Auto-scroll to today or first upcoming day
424
341
  const scrollTargetIndex = useMemo(() => {
425
342
  const tIdx = days.findIndex((d) => isToday(d.date));
426
343
  if (tIdx >= 0) return tIdx;
@@ -432,11 +349,8 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
432
349
 
433
350
  useEffect(() => {
434
351
  if (scrollRef.current && scrollTargetIndex > 0) {
435
- const container = scrollRef.current;
436
- const target = container.children[scrollTargetIndex] as HTMLElement;
437
- if (target) {
438
- container.scrollTo({ left: target.offsetLeft - 20, behavior: "smooth" });
439
- }
352
+ const target = scrollRef.current.children[scrollTargetIndex] as HTMLElement;
353
+ if (target) scrollRef.current.scrollTo({ left: target.offsetLeft - 16, behavior: "smooth" });
440
354
  }
441
355
  }, [scrollTargetIndex, days]);
442
356
 
@@ -453,42 +367,33 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
453
367
  if (error || matches.length === 0) return null;
454
368
 
455
369
  return (
456
- <div className="relative z-30 w-full mt-6 mb-4">
457
- {/* Tournament info header */}
370
+ <div className="relative z-30 w-full mt-5 mb-3">
458
371
  {tournament && (
459
- <div className="px-5 mb-3">
460
- <div className="flex items-center gap-2 mb-1">
461
- <Trophy size={14} style={{ color: "#22E3E8" }} />
462
- <span className="barlowcondensedBold text-[16px] tracking-wide text-white">
463
- {tournament.name}
464
- </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>
465
376
  </div>
466
377
  <div className="flex items-center gap-3">
467
378
  {tournament.start_date && tournament.end_date && (
468
- <span className="text-[11px] text-white" style={OUTFIT}>
379
+ <span className="text-[10px] text-white/50" style={OUTFIT}>
469
380
  {formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
470
381
  </span>
471
382
  )}
472
- <span className="text-[11px] text-white" style={OUTFIT}>
473
- {matches.length} matches announced
474
- </span>
383
+ <span className="text-[10px] text-white/50" style={OUTFIT}>{matches.length} matches</span>
475
384
  </div>
476
385
  </div>
477
386
  )}
478
387
 
479
- {/* Section header */}
480
- <div className="flex items-center gap-2 px-5 mb-3">
481
- <Calendar size={13} style={{ color: "#22E3E8" }} />
482
- <span className="barlowCondensedSemiBold text-[14px] tracking-wider text-white">
483
- SCHEDULE
484
- </span>
485
- <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)" }} />
486
392
  </div>
487
393
 
488
- {/* Scrollable day columns */}
489
394
  <div
490
395
  ref={scrollRef}
491
- 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"
492
397
  style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
493
398
  >
494
399
  {days.map((day, i) => (