@devrongx/games 0.2.1 → 0.3.1

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.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
package/src/index.ts CHANGED
@@ -11,4 +11,4 @@ export type { IChallengeConfig, IChallengeMarket, IChallengeOption, IChallengeTe
11
11
 
12
12
  // Matches — TD match calendar, data fetching, and types
13
13
  export { configureTDClient, fetchTDMatches, useTDMatches, MatchCalendar } from "./matches";
14
- export type { ITDClientConfig, ITDMatch, ITDTeam, ITDPlayer, ITDVenue, ITDTournament, ITDMatchesResponse, ITDMatchesParams, UseTDMatchesResult } from "./matches";
14
+ export type { ITDClientConfig, ITDMatch, ITDTeam, ITDPlayer, ITDVenue, ITDTournament, ITDStage, ITDMatchesResponse, ITDMatchesParams, UseTDMatchesResult } from "./matches";
@@ -1,9 +1,9 @@
1
1
  // @devrongx/games — matches/MatchCalendar.tsx
2
2
  "use client";
3
3
 
4
- import { useRef } from "react";
4
+ import { useRef, useMemo, useEffect } from "react";
5
5
  import { motion } from "framer-motion";
6
- import { Calendar, MapPin, Loader2, Zap } from "lucide-react";
6
+ import { Calendar, MapPin, Loader2, Trophy, Zap } from "lucide-react";
7
7
  import { useTDMatches } from "./useTDMatches";
8
8
  import type { ITDMatch } from "./types";
9
9
 
@@ -14,12 +14,44 @@ const MATCH_STATUS = {
14
14
  COMPLETED: 5,
15
15
  } as const;
16
16
 
17
- function formatMatchDate(iso: string): { dayMonth: string; time: string } {
18
- const d = new Date(iso);
19
- const day = d.getDate();
20
- const month = d.toLocaleString("en-US", { month: "short" }).toUpperCase();
21
- const time = d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
22
- return { dayMonth: `${day} ${month}`, time };
17
+ const OUTFIT = { fontFamily: "Outfit, sans-serif" };
18
+
19
+ /* ── Date helpers ── */
20
+
21
+ function toLocalDate(iso: string): Date {
22
+ return new Date(iso);
23
+ }
24
+
25
+ function formatDayMonth(d: Date): string {
26
+ return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`;
27
+ }
28
+
29
+ function formatWeekday(d: Date): string {
30
+ return d.toLocaleString("en-US", { weekday: "short" }).toUpperCase();
31
+ }
32
+
33
+ function formatTime(d: Date): string {
34
+ return d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
35
+ }
36
+
37
+ function isSameDay(a: Date, b: Date): boolean {
38
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
39
+ }
40
+
41
+ function isToday(d: Date): boolean {
42
+ return isSameDay(d, new Date());
43
+ }
44
+
45
+ function isTomorrow(d: Date): boolean {
46
+ const t = new Date();
47
+ t.setDate(t.getDate() + 1);
48
+ return isSameDay(d, t);
49
+ }
50
+
51
+ function getDayLabel(d: Date): string {
52
+ if (isToday(d)) return "TODAY";
53
+ if (isTomorrow(d)) return "TOMORROW";
54
+ return formatWeekday(d);
23
55
  }
24
56
 
25
57
  function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
@@ -27,144 +59,222 @@ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
27
59
  return t.short_name || t.name.slice(0, 3).toUpperCase();
28
60
  }
29
61
 
30
- interface MatchCardProps {
31
- match: ITDMatch;
32
- index: number;
62
+ /** Map popularity 0-10 to border shine opacity (0 = no shine, 10 = full intensity) */
63
+ function getShineOpacity(popularity: number): number {
64
+ if (popularity <= 0) return 0;
65
+ return Math.min(popularity / 10, 1);
33
66
  }
34
67
 
35
- function MatchCard({ match, index }: MatchCardProps) {
36
- const date = match.scheduled_start_at ? formatMatchDate(match.scheduled_start_at) : null;
68
+ /* ── Grouping: by date ── */
69
+
70
+ interface IMatchDay {
71
+ dateKey: string;
72
+ date: Date;
73
+ label: string;
74
+ dayMonth: string;
75
+ matches: ITDMatch[];
76
+ }
77
+
78
+ function groupByDate(matches: ITDMatch[]): IMatchDay[] {
79
+ const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
80
+
81
+ for (const m of matches) {
82
+ if (!m.scheduled_start_at) continue;
83
+ const d = toLocalDate(m.scheduled_start_at);
84
+ const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
85
+ const existing = map.get(key);
86
+ if (existing) {
87
+ existing.matches.push(m);
88
+ } else {
89
+ map.set(key, { date: d, matches: [m] });
90
+ }
91
+ }
92
+
93
+ return Array.from(map.entries())
94
+ .sort(([a], [b]) => a.localeCompare(b))
95
+ .map(([dateKey, { date, matches: dayMatches }]) => ({
96
+ dateKey,
97
+ date,
98
+ label: getDayLabel(date),
99
+ dayMonth: formatDayMonth(date),
100
+ matches: dayMatches,
101
+ }));
102
+ }
103
+
104
+ /* ── Match card with border shine animation ── */
105
+
106
+ function MatchCard({ match }: { match: ITDMatch }) {
37
107
  const isLive = match.status === MATCH_STATUS.LIVE;
38
108
  const isCompleted = match.status === MATCH_STATUS.COMPLETED;
39
- const isUpcoming = match.status === MATCH_STATUS.UPCOMING;
40
- const venue = match.venue?.city || match.venue?.name;
109
+ const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
110
+ const venue = match.venue?.city?.trim() || match.venue?.name;
111
+ const shineOpacity = getShineOpacity(match.rating_popularity);
112
+ const showShine = shineOpacity > 0 && !isCompleted;
41
113
 
42
114
  return (
43
- <motion.div
44
- initial={{ opacity: 0, y: 12 }}
45
- animate={{ opacity: 1, y: 0 }}
46
- transition={{ duration: 0.35, delay: index * 0.04 }}
47
- className="shrink-0"
48
- style={{ scrollSnapAlign: "start" }}
49
- >
115
+ <div className="relative w-full rounded-xl p-[1px] overflow-hidden">
116
+ {/* Spinning border shine intensity based on popularity */}
117
+ {showShine && (
118
+ <div
119
+ className="absolute inset-[-50%] pointer-events-none"
120
+ style={{
121
+ background: isLive
122
+ ? `conic-gradient(from 0deg, transparent 0%, transparent 65%, rgba(248,60,197,${shineOpacity * 0.6}) 75%, rgba(255,255,255,${shineOpacity * 0.7}) 80%, rgba(248,60,197,${shineOpacity * 0.6}) 85%, transparent 95%, transparent 100%)`
123
+ : `conic-gradient(from 0deg, transparent 0%, transparent 70%, rgba(34,227,232,${shineOpacity * 0.5}) 78%, rgba(255,255,255,${shineOpacity * 0.6}) 80%, rgba(34,227,232,${shineOpacity * 0.5}) 82%, transparent 90%, transparent 100%)`,
124
+ animation: `borderShine ${6 - shineOpacity * 2}s linear infinite`,
125
+ }}
126
+ />
127
+ )}
128
+
129
+ {/* Static border for no-shine / completed */}
130
+ {!showShine && (
131
+ <div
132
+ className="absolute inset-0 rounded-xl"
133
+ style={{
134
+ border: isCompleted
135
+ ? "1px solid rgba(255,255,255,0.06)"
136
+ : "1px solid rgba(255,255,255,0.08)",
137
+ }}
138
+ />
139
+ )}
140
+
50
141
  <div
51
- className="relative w-[140px] rounded-2xl overflow-hidden"
142
+ className="relative flex flex-col gap-1.5 rounded-xl px-3 py-2.5"
52
143
  style={{
53
- background: isLive
54
- ? "linear-gradient(160deg, rgba(248,60,197,0.15), rgba(248,60,197,0.03))"
55
- : isCompleted
56
- ? "rgba(255,255,255,0.03)"
57
- : "linear-gradient(160deg, rgba(34,227,232,0.08), rgba(153,69,255,0.04))",
58
- border: isLive
59
- ? "1px solid rgba(248,60,197,0.3)"
60
- : isCompleted
61
- ? "1px solid rgba(255,255,255,0.06)"
62
- : "1px solid rgba(34,227,232,0.12)",
144
+ background: isLive ? "rgba(10,10,18,0.95)" : "rgba(10,10,18,0.98)",
63
145
  }}
64
146
  >
65
- {/* Live pulse indicator */}
66
- {isLive && (
67
- <div className="absolute top-2 right-2 flex items-center gap-1">
68
- <span className="relative flex h-2 w-2">
69
- <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
70
- <span className="relative inline-flex rounded-full h-2 w-2 bg-[#f83cc5]" />
71
- </span>
72
- </div>
73
- )}
74
-
75
- <div className="flex flex-col items-center px-3 pt-3 pb-2.5">
76
- {/* Date */}
77
- {date && (
78
- <div className="flex flex-col items-center mb-2.5">
79
- <span
80
- className="text-[11px] font-semibold tracking-wide"
81
- style={{
82
- fontFamily: "Outfit, sans-serif",
83
- color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.25)" : "rgba(34,227,232,0.7)",
84
- }}
85
- >
86
- {date.dayMonth}
147
+ {/* Time + live indicator */}
148
+ <div className="flex items-center justify-between">
149
+ <span
150
+ className="text-[10px]"
151
+ style={{
152
+ ...OUTFIT,
153
+ color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.35)" : "#fff",
154
+ }}
155
+ >
156
+ {time}
157
+ </span>
158
+ {isLive && (
159
+ <div className="flex items-center gap-1">
160
+ <span className="relative flex h-1.5 w-1.5">
161
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
162
+ <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
87
163
  </span>
88
- <span
89
- className="text-[10px] tracking-wide"
90
- style={{
91
- fontFamily: "Outfit, sans-serif",
92
- color: isCompleted ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.35)",
93
- }}
94
- >
95
- {date.time}
164
+ <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
165
+ LIVE
96
166
  </span>
97
167
  </div>
98
168
  )}
99
-
100
- {/* Teams */}
101
- <div className="flex items-center gap-1.5 w-full justify-center">
102
- <span
103
- className="barlowcondensedBold text-[22px] leading-none"
104
- style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
105
- >
106
- {getTeamShortName(match, "a")}
107
- </span>
108
- <span
109
- className="text-[9px] barlowCondensedSemiBold"
110
- style={{ color: isCompleted ? "rgba(255,255,255,0.15)" : "rgba(255,255,255,0.25)" }}
111
- >
112
- v
169
+ {isCompleted && match.winner && (
170
+ <span className="text-[8px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.35)" }}>
171
+ {match.winner.short_name || match.winner.name} won
113
172
  </span>
173
+ )}
174
+ </div>
175
+
176
+ {/* Teams */}
177
+ <div className="flex items-center gap-1.5 justify-center">
178
+ <span
179
+ className="barlowcondensedBold text-[20px] leading-none"
180
+ style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
181
+ >
182
+ {getTeamShortName(match, "a")}
183
+ </span>
184
+ <span className="text-[9px]" style={{ color: "rgba(255,255,255,0.3)" }}>
185
+ v
186
+ </span>
187
+ <span
188
+ className="barlowcondensedBold text-[20px] leading-none"
189
+ style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
190
+ >
191
+ {getTeamShortName(match, "b")}
192
+ </span>
193
+ </div>
194
+
195
+ {/* Venue */}
196
+ {venue && (
197
+ <div className="flex items-center gap-1 justify-center">
198
+ <MapPin size={8} style={{ color: "rgba(255,255,255,0.3)" }} />
114
199
  <span
115
- className="barlowcondensedBold text-[22px] leading-none"
116
- style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
200
+ className="text-[8px] truncate max-w-[100px]"
201
+ style={{ ...OUTFIT, color: "rgba(255,255,255,0.3)" }}
117
202
  >
118
- {getTeamShortName(match, "b")}
203
+ {venue}
119
204
  </span>
120
205
  </div>
206
+ )}
121
207
 
122
- {/* Venue */}
123
- {venue && (
124
- <div className="flex items-center gap-1 mt-1.5">
125
- <MapPin size={8} className="shrink-0" style={{ color: "rgba(255,255,255,0.2)" }} />
126
- <span
127
- className="text-[9px] truncate max-w-[110px]"
128
- style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
129
- >
130
- {venue}
131
- </span>
132
- </div>
133
- )}
134
-
135
- {/* Status */}
136
- <div className="mt-2">
137
- {isLive ? (
138
- <div className="flex items-center gap-1 px-2 py-0.5 rounded-full" style={{ background: "rgba(248,60,197,0.15)" }}>
139
- <Zap size={8} style={{ color: "#f83cc5" }} />
140
- <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
141
- LIVE
142
- </span>
143
- </div>
144
- ) : isCompleted ? (
145
- <span
146
- className="text-[9px] tracking-wide"
147
- style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
148
- >
149
- {match.winner ? `${match.winner.short_name || match.winner.name} won` : "Completed"}
150
- </span>
151
- ) : isUpcoming ? (
152
- <div
153
- className="px-2 py-0.5 rounded-full"
154
- style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.15)" }}
155
- >
156
- <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "rgba(34,227,232,0.6)" }}>
157
- UPCOMING
158
- </span>
159
- </div>
160
- ) : null}
208
+ {/* Popularity indicator */}
209
+ {match.rating_popularity > 0 && !isCompleted && (
210
+ <div className="flex items-center gap-0.5 justify-center mt-0.5">
211
+ {Array.from({ length: Math.min(Math.ceil(match.rating_popularity / 2), 5) }).map((_, i) => (
212
+ <Zap
213
+ key={i}
214
+ size={7}
215
+ fill={isLive ? "#f83cc5" : "#22E3E8"}
216
+ style={{ color: isLive ? "#f83cc5" : "#22E3E8", opacity: 0.4 + i * 0.15 }}
217
+ />
218
+ ))}
161
219
  </div>
162
- </div>
220
+ )}
163
221
  </div>
222
+ </div>
223
+ );
224
+ }
225
+
226
+ /* ── Day column ── */
227
+
228
+ function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
229
+ const hasToday = day.label === "TODAY";
230
+ const hasLive = day.matches.some((m) => m.status === MATCH_STATUS.LIVE);
231
+ const allCompleted = day.matches.every((m) => m.status === MATCH_STATUS.COMPLETED);
232
+
233
+ return (
234
+ <motion.div
235
+ initial={{ opacity: 0, y: 10 }}
236
+ animate={{ opacity: 1, y: 0 }}
237
+ transition={{ duration: 0.3, delay: index * 0.03 }}
238
+ className="shrink-0 flex flex-col gap-1.5"
239
+ style={{ scrollSnapAlign: "start", width: "140px" }}
240
+ >
241
+ {/* Day header */}
242
+ <div className="flex flex-col items-center gap-0.5 pb-1">
243
+ <span
244
+ className="text-[10px] barlowcondensedBold tracking-widest"
245
+ style={{
246
+ color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.25)" : "#fff",
247
+ }}
248
+ >
249
+ {day.label}
250
+ </span>
251
+ <span
252
+ className="text-[12px] font-semibold"
253
+ style={{
254
+ ...OUTFIT,
255
+ color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.25)" : "#fff",
256
+ }}
257
+ >
258
+ {day.dayMonth}
259
+ </span>
260
+ {(hasToday || hasLive) && (
261
+ <div
262
+ className="w-5 h-[2px] rounded-full mt-0.5"
263
+ style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
264
+ />
265
+ )}
266
+ </div>
267
+
268
+ {/* Match cards for the day */}
269
+ {day.matches.map((match) => (
270
+ <MatchCard key={match.id} match={match} />
271
+ ))}
164
272
  </motion.div>
165
273
  );
166
274
  }
167
275
 
276
+ /* ── Main calendar component ── */
277
+
168
278
  interface MatchCalendarProps {
169
279
  tournamentId?: number;
170
280
  }
@@ -173,13 +283,48 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
173
283
  const scrollRef = useRef<HTMLDivElement>(null);
174
284
  const { matches, isLoading, error } = useTDMatches({
175
285
  tournamentId,
176
- limit: 50,
286
+ limit: 100,
177
287
  });
178
288
 
289
+ const days = useMemo(() => groupByDate(matches), [matches]);
290
+
291
+ // Auto-scroll to today or first upcoming day
292
+ const scrollTargetIndex = useMemo(() => {
293
+ const tIdx = days.findIndex((d) => isToday(d.date));
294
+ if (tIdx >= 0) return tIdx;
295
+ const now = new Date();
296
+ const uIdx = days.findIndex((d) => d.date >= now);
297
+ if (uIdx >= 0) return Math.max(0, uIdx);
298
+ return 0;
299
+ }, [days]);
300
+
301
+ useEffect(() => {
302
+ if (scrollRef.current && scrollTargetIndex > 0) {
303
+ const container = scrollRef.current;
304
+ const target = container.children[scrollTargetIndex] as HTMLElement;
305
+ if (target) {
306
+ container.scrollTo({ left: target.offsetLeft - 20, behavior: "smooth" });
307
+ }
308
+ }
309
+ }, [scrollTargetIndex, days]);
310
+
311
+ // Stage groups
312
+ const stageGroups = useMemo(() => {
313
+ const stages = new Map<number, { name: string; orderNo: number }>();
314
+ for (const m of matches) {
315
+ if (m.stage && !stages.has(m.stage.id)) {
316
+ stages.set(m.stage.id, { name: m.stage.name, orderNo: m.stage.order_no });
317
+ }
318
+ }
319
+ return Array.from(stages.values()).sort((a, b) => a.orderNo - b.orderNo);
320
+ }, [matches]);
321
+
322
+ const tournament = matches[0]?.tournament;
323
+
179
324
  if (isLoading) {
180
325
  return (
181
- <div className="flex items-center justify-center py-4">
182
- <Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.2)" }} />
326
+ <div className="flex items-center justify-center py-6">
327
+ <Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} />
183
328
  </div>
184
329
  );
185
330
  }
@@ -187,27 +332,58 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
187
332
  if (error || matches.length === 0) return null;
188
333
 
189
334
  return (
190
- <div className="relative z-30 w-full mb-2 mt-2">
335
+ <div className="relative z-30 w-full mt-6 mb-4">
336
+ {/* Tournament info header */}
337
+ {tournament && (
338
+ <div className="px-5 mb-3">
339
+ <div className="flex items-center gap-2 mb-1">
340
+ <Trophy size={14} style={{ color: "#22E3E8" }} />
341
+ <span className="barlowcondensedBold text-[16px] tracking-wide text-white">
342
+ {tournament.name}
343
+ </span>
344
+ </div>
345
+ <div className="flex items-center gap-3">
346
+ {tournament.start_date && tournament.end_date && (
347
+ <span className="text-[11px] text-white" style={OUTFIT}>
348
+ {formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
349
+ </span>
350
+ )}
351
+ <span className="text-[11px] text-white" style={OUTFIT}>
352
+ {matches.length} matches
353
+ </span>
354
+ {stageGroups.length > 0 && (
355
+ <span
356
+ className="text-[9px] barlowcondensedBold tracking-widest px-2 py-0.5 rounded-full"
357
+ style={{
358
+ color: "#22E3E8",
359
+ background: "rgba(34,227,232,0.08)",
360
+ border: "1px solid rgba(34,227,232,0.15)",
361
+ }}
362
+ >
363
+ {stageGroups[0].name.toUpperCase()}
364
+ </span>
365
+ )}
366
+ </div>
367
+ </div>
368
+ )}
369
+
191
370
  {/* Section header */}
192
371
  <div className="flex items-center gap-2 px-5 mb-2.5">
193
- <Calendar size={12} style={{ color: "rgba(34,227,232,0.6)" }} />
194
- <span className="barlowCondensedSemiBold text-[13px] tracking-wider" style={{ color: "rgba(255,255,255,0.45)" }}>
195
- MATCHES
196
- </span>
197
- <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.04)" }} />
198
- <span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
199
- {matches.length}
372
+ <Calendar size={12} style={{ color: "#22E3E8" }} />
373
+ <span className="barlowCondensedSemiBold text-[13px] tracking-wider text-white">
374
+ SCHEDULE
200
375
  </span>
376
+ <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
201
377
  </div>
202
378
 
203
- {/* Scrollable match strip */}
379
+ {/* Scrollable day columns */}
204
380
  <div
205
381
  ref={scrollRef}
206
- className="flex gap-2.5 overflow-x-auto px-5 pb-2 scrollbar-hidden"
382
+ className="flex gap-2.5 overflow-x-auto px-5 pb-3 scrollbar-hide"
207
383
  style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
208
384
  >
209
- {matches.map((match, i) => (
210
- <MatchCard key={match.id} match={match} index={i} />
385
+ {days.map((day, i) => (
386
+ <DayColumn key={day.dateKey} day={day} index={i} />
211
387
  ))}
212
388
  </div>
213
389
  </div>
@@ -11,6 +11,7 @@ export type {
11
11
  ITDPlayer,
12
12
  ITDVenue,
13
13
  ITDTournament,
14
+ ITDStage,
14
15
  ITDMatchesResponse,
15
16
  ITDMatchesParams,
16
17
  } from "./types";
@@ -1,7 +1,8 @@
1
1
  // @devrongx/games — matches/types.ts
2
2
  // Types matching the Prisma response from TD's GET /api/v2/matches exactly.
3
3
  // listMatches includes: { team_a: true, team_b: true, winner: true, venue: true,
4
- // man_of_the_match: true, tournament: { select: { id, name } } }
4
+ // man_of_the_match: true, tournament: { select: { id, name, start_date, end_date } },
5
+ // stage: { select: { id, name, stage_type, order_no } } }
5
6
 
6
7
  // ── Prisma `teams` model (full row, returned by `include: { team_a: true }`) ──
7
8
  export interface ITDTeam {
@@ -52,10 +53,20 @@ export interface ITDVenue {
52
53
  updated_at: string;
53
54
  }
54
55
 
55
- // ── Prisma `tournaments` model (partial, returned by `select: { id, name }`) ──
56
+ // ── Prisma `tournaments` model (partial, returned by `select: { id, name, start_date, end_date }`) ──
56
57
  export interface ITDTournament {
57
58
  id: number;
58
59
  name: string;
60
+ start_date: string | null;
61
+ end_date: string | null;
62
+ }
63
+
64
+ // ── Prisma `stages` model (partial, returned by `select: { id, name, stage_type, order_no }`) ──
65
+ export interface ITDStage {
66
+ id: number;
67
+ name: string;
68
+ stage_type: number;
69
+ order_no: number;
59
70
  }
60
71
 
61
72
  // ── Prisma `matches` model with included relations ──
@@ -103,6 +114,7 @@ export interface ITDMatch {
103
114
  venue: ITDVenue | null;
104
115
  man_of_the_match: ITDPlayer | null;
105
116
  tournament: ITDTournament | null;
117
+ stage: ITDStage | null;
106
118
  }
107
119
 
108
120
  // ── API response shape ──
@@ -115,6 +127,7 @@ export interface ITDMatchesResponse {
115
127
 
116
128
  export interface ITDMatchesParams {
117
129
  tournamentId?: number;
130
+ stageId?: number;
118
131
  status?: number;
119
132
  from?: string;
120
133
  to?: string;