@devrongx/games 0.2.0 → 0.3.0
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 +1 -1
- package/src/index.ts +1 -1
- package/src/matches/MatchCalendar.tsx +289 -93
- package/src/matches/index.ts +1 -0
- package/src/matches/types.ts +15 -2
package/package.json
CHANGED
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,14 +1,12 @@
|
|
|
1
1
|
// @devrongx/games — matches/MatchCalendar.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { useRef } from "react";
|
|
4
|
+
import { useRef, useMemo } from "react";
|
|
5
5
|
import { motion } from "framer-motion";
|
|
6
|
-
import { Calendar, MapPin, Loader2 } from "lucide-react";
|
|
6
|
+
import { Calendar, MapPin, Loader2, Zap, ChevronRight } from "lucide-react";
|
|
7
7
|
import { useTDMatches } from "./useTDMatches";
|
|
8
8
|
import type { ITDMatch } from "./types";
|
|
9
9
|
|
|
10
|
-
const OUTFIT = { fontFamily: "Outfit, sans-serif" };
|
|
11
|
-
|
|
12
10
|
// TD match status enum values
|
|
13
11
|
const MATCH_STATUS = {
|
|
14
12
|
UPCOMING: 1,
|
|
@@ -16,20 +14,42 @@ const MATCH_STATUS = {
|
|
|
16
14
|
COMPLETED: 5,
|
|
17
15
|
} as const;
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
month: d.toLocaleString("en-US", { month: "short" }).toUpperCase(),
|
|
24
|
-
time: d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }),
|
|
25
|
-
weekday: d.toLocaleString("en-US", { weekday: "short" }).toUpperCase(),
|
|
26
|
-
};
|
|
17
|
+
/* ── Date helpers ── */
|
|
18
|
+
|
|
19
|
+
function toLocalDate(iso: string): Date {
|
|
20
|
+
return new Date(iso);
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
function formatDayMonth(d: Date): string {
|
|
24
|
+
return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatWeekday(d: Date): string {
|
|
28
|
+
return d.toLocaleString("en-US", { weekday: "short" }).toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatTime(d: Date): string {
|
|
32
|
+
return d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isSameDay(a: Date, b: Date): boolean {
|
|
36
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isToday(d: Date): boolean {
|
|
40
|
+
return isSameDay(d, new Date());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isTomorrow(d: Date): boolean {
|
|
44
|
+
const t = new Date();
|
|
45
|
+
t.setDate(t.getDate() + 1);
|
|
46
|
+
return isSameDay(d, t);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getDayLabel(d: Date): string {
|
|
50
|
+
if (isToday(d)) return "TODAY";
|
|
51
|
+
if (isTomorrow(d)) return "TOMORROW";
|
|
52
|
+
return formatWeekday(d);
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
@@ -37,86 +57,200 @@ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
|
37
57
|
return t.short_name || t.name.slice(0, 3).toUpperCase();
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
/* ── Grouping: by date ── */
|
|
61
|
+
|
|
62
|
+
interface IMatchDay {
|
|
63
|
+
dateKey: string; // YYYY-MM-DD
|
|
64
|
+
date: Date;
|
|
65
|
+
label: string; // "TODAY", "TOMORROW", "SAT", etc
|
|
66
|
+
dayMonth: string; // "28 MAR"
|
|
67
|
+
matches: ITDMatch[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function groupByDate(matches: ITDMatch[]): IMatchDay[] {
|
|
71
|
+
const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
|
|
72
|
+
|
|
73
|
+
for (const m of matches) {
|
|
74
|
+
if (!m.scheduled_start_at) continue;
|
|
75
|
+
const d = toLocalDate(m.scheduled_start_at);
|
|
76
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
77
|
+
const existing = map.get(key);
|
|
78
|
+
if (existing) {
|
|
79
|
+
existing.matches.push(m);
|
|
80
|
+
} else {
|
|
81
|
+
map.set(key, { date: d, matches: [m] });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Array.from(map.entries())
|
|
86
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
87
|
+
.map(([dateKey, { date, matches: dayMatches }]) => ({
|
|
88
|
+
dateKey,
|
|
89
|
+
date,
|
|
90
|
+
label: getDayLabel(date),
|
|
91
|
+
dayMonth: formatDayMonth(date),
|
|
92
|
+
matches: dayMatches,
|
|
93
|
+
}));
|
|
42
94
|
}
|
|
43
95
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
96
|
+
/* ── Mini match row inside a day column ── */
|
|
97
|
+
|
|
98
|
+
function MatchRow({ match }: { match: ITDMatch }) {
|
|
99
|
+
const isLive = match.status === MATCH_STATUS.LIVE;
|
|
47
100
|
const isCompleted = match.status === MATCH_STATUS.COMPLETED;
|
|
101
|
+
const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
|
|
102
|
+
const venue = match.venue?.city || match.venue?.name;
|
|
48
103
|
|
|
49
104
|
return (
|
|
50
105
|
<div
|
|
51
|
-
className="
|
|
106
|
+
className="flex flex-col gap-1 rounded-xl px-3 py-2.5"
|
|
52
107
|
style={{
|
|
53
|
-
background:
|
|
54
|
-
? "rgba(
|
|
55
|
-
:
|
|
108
|
+
background: isLive
|
|
109
|
+
? "rgba(248,60,197,0.08)"
|
|
110
|
+
: isCompleted
|
|
111
|
+
? "rgba(255,255,255,0.02)"
|
|
112
|
+
: "rgba(255,255,255,0.03)",
|
|
113
|
+
border: isLive
|
|
114
|
+
? "1px solid rgba(248,60,197,0.2)"
|
|
115
|
+
: "1px solid rgba(255,255,255,0.04)",
|
|
56
116
|
}}
|
|
57
117
|
>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
{/* Time + status row */}
|
|
119
|
+
<div className="flex items-center justify-between">
|
|
120
|
+
<span
|
|
121
|
+
className="text-[10px]"
|
|
122
|
+
style={{
|
|
123
|
+
fontFamily: "Outfit, sans-serif",
|
|
124
|
+
color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.4)",
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{time}
|
|
128
|
+
</span>
|
|
129
|
+
{isLive && (
|
|
130
|
+
<div className="flex items-center gap-1">
|
|
131
|
+
<span className="relative flex h-1.5 w-1.5">
|
|
132
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
|
|
133
|
+
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
|
|
134
|
+
</span>
|
|
135
|
+
<span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
|
|
136
|
+
LIVE
|
|
68
137
|
</span>
|
|
69
138
|
</div>
|
|
70
139
|
)}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<span className="text-[10px] text-white/30 barlowCondensedSemiBold">VS</span>
|
|
78
|
-
<span className="barlowcondensedBold text-[20px] text-white leading-none">
|
|
79
|
-
{getTeamShortName(match, "b")}
|
|
140
|
+
{isCompleted && match.winner && (
|
|
141
|
+
<span
|
|
142
|
+
className="text-[8px] tracking-wide"
|
|
143
|
+
style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
|
|
144
|
+
>
|
|
145
|
+
{match.winner.short_name || match.winner.name} won
|
|
80
146
|
</span>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
{/* Venue */}
|
|
84
|
-
{match.venue && (
|
|
85
|
-
<div className="flex items-center gap-1 mt-1.5">
|
|
86
|
-
<MapPin size={8} className="text-white/30 shrink-0" />
|
|
87
|
-
<span
|
|
88
|
-
className="text-[9px] text-white/30 truncate max-w-[130px]"
|
|
89
|
-
style={OUTFIT}
|
|
90
|
-
>
|
|
91
|
-
{match.venue.city || match.venue.name}
|
|
92
|
-
</span>
|
|
93
|
-
</div>
|
|
94
147
|
)}
|
|
148
|
+
</div>
|
|
95
149
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
150
|
+
{/* Teams */}
|
|
151
|
+
<div className="flex items-center gap-1.5 justify-center">
|
|
152
|
+
<span
|
|
153
|
+
className="barlowcondensedBold text-[18px] leading-none"
|
|
154
|
+
style={{ color: isCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
|
|
100
155
|
>
|
|
156
|
+
{getTeamShortName(match, "a")}
|
|
157
|
+
</span>
|
|
158
|
+
<span className="text-[9px]" style={{ color: "rgba(255,255,255,0.2)" }}>
|
|
159
|
+
v
|
|
160
|
+
</span>
|
|
161
|
+
<span
|
|
162
|
+
className="barlowcondensedBold text-[18px] leading-none"
|
|
163
|
+
style={{ color: isCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
|
|
164
|
+
>
|
|
165
|
+
{getTeamShortName(match, "b")}
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{/* Venue */}
|
|
170
|
+
{venue && (
|
|
171
|
+
<div className="flex items-center gap-1 justify-center">
|
|
172
|
+
<MapPin size={7} style={{ color: "rgba(255,255,255,0.15)" }} />
|
|
101
173
|
<span
|
|
102
|
-
className="text-[
|
|
103
|
-
style={{ color:
|
|
174
|
+
className="text-[8px] truncate max-w-[100px]"
|
|
175
|
+
style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.15)" }}
|
|
104
176
|
>
|
|
105
|
-
{
|
|
177
|
+
{venue}
|
|
106
178
|
</span>
|
|
107
179
|
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
108
184
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
185
|
+
/* ── Day column ── */
|
|
186
|
+
|
|
187
|
+
function DayColumn({ day, index }: { day: IMatchDay; index: number }) {
|
|
188
|
+
const hasToday = day.label === "TODAY";
|
|
189
|
+
const hasLive = day.matches.some((m) => m.status === MATCH_STATUS.LIVE);
|
|
190
|
+
const allCompleted = day.matches.every((m) => m.status === MATCH_STATUS.COMPLETED);
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<motion.div
|
|
194
|
+
initial={{ opacity: 0, y: 10 }}
|
|
195
|
+
animate={{ opacity: 1, y: 0 }}
|
|
196
|
+
transition={{ duration: 0.3, delay: index * 0.03 }}
|
|
197
|
+
className="shrink-0 flex flex-col gap-1.5"
|
|
198
|
+
style={{ scrollSnapAlign: "start", width: "130px" }}
|
|
199
|
+
>
|
|
200
|
+
{/* Day header */}
|
|
201
|
+
<div className="flex flex-col items-center gap-0.5 pb-1">
|
|
202
|
+
<span
|
|
203
|
+
className="text-[10px] barlowcondensedBold tracking-widest"
|
|
204
|
+
style={{
|
|
205
|
+
color: hasLive
|
|
206
|
+
? "#f83cc5"
|
|
207
|
+
: hasToday
|
|
208
|
+
? "#22E3E8"
|
|
209
|
+
: allCompleted
|
|
210
|
+
? "rgba(255,255,255,0.15)"
|
|
211
|
+
: "rgba(255,255,255,0.4)",
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{day.label}
|
|
215
|
+
</span>
|
|
216
|
+
<span
|
|
217
|
+
className="text-[11px] font-semibold"
|
|
218
|
+
style={{
|
|
219
|
+
fontFamily: "Outfit, sans-serif",
|
|
220
|
+
color: hasLive
|
|
221
|
+
? "rgba(248,60,197,0.8)"
|
|
222
|
+
: hasToday
|
|
223
|
+
? "rgba(34,227,232,0.7)"
|
|
224
|
+
: allCompleted
|
|
225
|
+
? "rgba(255,255,255,0.15)"
|
|
226
|
+
: "rgba(255,255,255,0.5)",
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
{day.dayMonth}
|
|
230
|
+
</span>
|
|
231
|
+
{/* Indicator dot */}
|
|
232
|
+
{(hasToday || hasLive) && (
|
|
233
|
+
<div
|
|
234
|
+
className="w-5 h-[2px] rounded-full mt-0.5"
|
|
235
|
+
style={{
|
|
236
|
+
background: hasLive
|
|
237
|
+
? "#f83cc5"
|
|
238
|
+
: "#22E3E8",
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
114
241
|
)}
|
|
115
242
|
</div>
|
|
116
|
-
|
|
243
|
+
|
|
244
|
+
{/* Match cards for the day */}
|
|
245
|
+
{day.matches.map((match) => (
|
|
246
|
+
<MatchRow key={match.id} match={match} />
|
|
247
|
+
))}
|
|
248
|
+
</motion.div>
|
|
117
249
|
);
|
|
118
250
|
}
|
|
119
251
|
|
|
252
|
+
/* ── Main calendar component ── */
|
|
253
|
+
|
|
120
254
|
interface MatchCalendarProps {
|
|
121
255
|
tournamentId?: number;
|
|
122
256
|
}
|
|
@@ -125,13 +259,53 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
|
125
259
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
126
260
|
const { matches, isLoading, error } = useTDMatches({
|
|
127
261
|
tournamentId,
|
|
128
|
-
limit:
|
|
262
|
+
limit: 100,
|
|
129
263
|
});
|
|
130
264
|
|
|
265
|
+
const days = useMemo(() => groupByDate(matches), [matches]);
|
|
266
|
+
|
|
267
|
+
// Find the index of today or the first upcoming day to auto-scroll
|
|
268
|
+
const todayIndex = useMemo(() => {
|
|
269
|
+
const now = new Date();
|
|
270
|
+
// Find today
|
|
271
|
+
const tIdx = days.findIndex((d) => isToday(d.date));
|
|
272
|
+
if (tIdx >= 0) return tIdx;
|
|
273
|
+
// Find first upcoming day (date >= today)
|
|
274
|
+
const uIdx = days.findIndex((d) => d.date >= now);
|
|
275
|
+
if (uIdx >= 0) return Math.max(0, uIdx - 1); // show one before so user sees context
|
|
276
|
+
return 0;
|
|
277
|
+
}, [days]);
|
|
278
|
+
|
|
279
|
+
// Stage info from first match (all matches in a tournamentId query share same stage typically)
|
|
280
|
+
const stageGroups = useMemo(() => {
|
|
281
|
+
const stages = new Map<number, { name: string; orderNo: number; days: IMatchDay[] }>();
|
|
282
|
+
for (const day of days) {
|
|
283
|
+
for (const m of day.matches) {
|
|
284
|
+
if (m.stage) {
|
|
285
|
+
if (!stages.has(m.stage.id)) {
|
|
286
|
+
stages.set(m.stage.id, { name: m.stage.name, orderNo: m.stage.order_no, days: [] });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Assign day to stage (use first match's stage)
|
|
291
|
+
const firstStage = day.matches.find((m) => m.stage)?.stage;
|
|
292
|
+
if (firstStage && stages.has(firstStage.id)) {
|
|
293
|
+
const group = stages.get(firstStage.id);
|
|
294
|
+
if (group && !group.days.includes(day)) {
|
|
295
|
+
group.days.push(day);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return Array.from(stages.values()).sort((a, b) => a.orderNo - b.orderNo);
|
|
300
|
+
}, [days]);
|
|
301
|
+
|
|
302
|
+
// Tournament date range from first match
|
|
303
|
+
const tournament = matches[0]?.tournament;
|
|
304
|
+
|
|
131
305
|
if (isLoading) {
|
|
132
306
|
return (
|
|
133
|
-
<div className="flex items-center justify-center py-
|
|
134
|
-
<Loader2 size={
|
|
307
|
+
<div className="flex items-center justify-center py-4">
|
|
308
|
+
<Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.2)" }} />
|
|
135
309
|
</div>
|
|
136
310
|
);
|
|
137
311
|
}
|
|
@@ -139,35 +313,57 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
|
139
313
|
if (error || matches.length === 0) return null;
|
|
140
314
|
|
|
141
315
|
return (
|
|
142
|
-
<div className="relative z-30 w-full mb-
|
|
316
|
+
<div className="relative z-30 w-full mb-2 mt-2">
|
|
143
317
|
{/* Section header */}
|
|
144
|
-
<div className="flex items-center gap-2 px-5 mb-
|
|
145
|
-
<Calendar size={
|
|
146
|
-
<span className="barlowCondensedSemiBold text-[
|
|
147
|
-
|
|
318
|
+
<div className="flex items-center gap-2 px-5 mb-1">
|
|
319
|
+
<Calendar size={12} style={{ color: "rgba(34,227,232,0.6)" }} />
|
|
320
|
+
<span className="barlowCondensedSemiBold text-[13px] tracking-wider" style={{ color: "rgba(255,255,255,0.45)" }}>
|
|
321
|
+
SCHEDULE
|
|
148
322
|
</span>
|
|
149
|
-
|
|
150
|
-
|
|
323
|
+
{/* Tournament date range */}
|
|
324
|
+
{tournament?.start_date && tournament?.end_date && (
|
|
325
|
+
<>
|
|
326
|
+
<ChevronRight size={10} style={{ color: "rgba(255,255,255,0.15)" }} />
|
|
327
|
+
<span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
|
|
328
|
+
{formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
|
|
329
|
+
</span>
|
|
330
|
+
</>
|
|
331
|
+
)}
|
|
332
|
+
<div className="flex-1" />
|
|
333
|
+
<span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
|
|
151
334
|
{matches.length} matches
|
|
152
335
|
</span>
|
|
153
336
|
</div>
|
|
154
337
|
|
|
155
|
-
{/*
|
|
338
|
+
{/* Stage label if exists */}
|
|
339
|
+
{stageGroups.length > 0 && stageGroups[0].name && (
|
|
340
|
+
<div className="px-5 mb-2">
|
|
341
|
+
<span
|
|
342
|
+
className="text-[9px] barlowcondensedBold tracking-widest px-2 py-0.5 rounded-full"
|
|
343
|
+
style={{
|
|
344
|
+
color: "rgba(34,227,232,0.5)",
|
|
345
|
+
background: "rgba(34,227,232,0.06)",
|
|
346
|
+
border: "1px solid rgba(34,227,232,0.1)",
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
{stageGroups[0].name.toUpperCase()}
|
|
350
|
+
</span>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Scrollable day columns */}
|
|
156
355
|
<div
|
|
157
356
|
ref={scrollRef}
|
|
158
|
-
className="flex gap-
|
|
159
|
-
style={{
|
|
357
|
+
className="flex gap-2 overflow-x-auto px-5 pb-2 scrollbar-hidden"
|
|
358
|
+
style={{
|
|
359
|
+
scrollSnapType: "x mandatory",
|
|
360
|
+
WebkitOverflowScrolling: "touch",
|
|
361
|
+
// Auto-scroll to today/upcoming
|
|
362
|
+
...(todayIndex > 0 ? {} : {}),
|
|
363
|
+
}}
|
|
160
364
|
>
|
|
161
|
-
{
|
|
162
|
-
<
|
|
163
|
-
key={match.id}
|
|
164
|
-
initial={{ opacity: 0, scale: 0.95 }}
|
|
165
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
166
|
-
transition={{ duration: 0.3 }}
|
|
167
|
-
style={{ scrollSnapAlign: "start" }}
|
|
168
|
-
>
|
|
169
|
-
<MatchCard match={match} />
|
|
170
|
-
</motion.div>
|
|
365
|
+
{days.map((day, i) => (
|
|
366
|
+
<DayColumn key={day.dateKey} day={day} index={i} />
|
|
171
367
|
))}
|
|
172
368
|
</div>
|
|
173
369
|
</div>
|
package/src/matches/index.ts
CHANGED
package/src/matches/types.ts
CHANGED
|
@@ -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;
|