@devrongx/games 0.2.1 → 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 +291 -135
- 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,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 } from "react";
|
|
5
5
|
import { motion } from "framer-motion";
|
|
6
|
-
import { Calendar, MapPin, Loader2, Zap } 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
|
|
|
@@ -14,12 +14,42 @@ const MATCH_STATUS = {
|
|
|
14
14
|
COMPLETED: 5,
|
|
15
15
|
} as const;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
/* ── Date helpers ── */
|
|
18
|
+
|
|
19
|
+
function toLocalDate(iso: string): Date {
|
|
20
|
+
return new Date(iso);
|
|
21
|
+
}
|
|
22
|
+
|
|
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);
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
@@ -27,144 +57,200 @@ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
|
27
57
|
return t.short_name || t.name.slice(0, 3).toUpperCase();
|
|
28
58
|
}
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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[];
|
|
33
68
|
}
|
|
34
69
|
|
|
35
|
-
function
|
|
36
|
-
const
|
|
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
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── Mini match row inside a day column ── */
|
|
97
|
+
|
|
98
|
+
function MatchRow({ match }: { match: ITDMatch }) {
|
|
37
99
|
const isLive = match.status === MATCH_STATUS.LIVE;
|
|
38
100
|
const isCompleted = match.status === MATCH_STATUS.COMPLETED;
|
|
39
|
-
const
|
|
101
|
+
const time = match.scheduled_start_at ? formatTime(toLocalDate(match.scheduled_start_at)) : "";
|
|
40
102
|
const venue = match.venue?.city || match.venue?.name;
|
|
41
103
|
|
|
42
104
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
105
|
+
<div
|
|
106
|
+
className="flex flex-col gap-1 rounded-xl px-3 py-2.5"
|
|
107
|
+
style={{
|
|
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)",
|
|
116
|
+
}}
|
|
49
117
|
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
? "1px solid rgba(255,255,255,0.06)"
|
|
62
|
-
: "1px solid rgba(34,227,232,0.12)",
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
{/* Live pulse indicator */}
|
|
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>
|
|
66
129
|
{isLive && (
|
|
67
|
-
<div className="
|
|
68
|
-
<span className="relative flex h-
|
|
130
|
+
<div className="flex items-center gap-1">
|
|
131
|
+
<span className="relative flex h-1.5 w-1.5">
|
|
69
132
|
<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-
|
|
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
|
|
71
137
|
</span>
|
|
72
138
|
</div>
|
|
73
139
|
)}
|
|
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
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
74
149
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}}
|
|
94
|
-
>
|
|
95
|
-
{date.time}
|
|
96
|
-
</span>
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
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
|
|
113
|
-
</span>
|
|
114
|
-
<span
|
|
115
|
-
className="barlowcondensedBold text-[22px] leading-none"
|
|
116
|
-
style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
|
|
117
|
-
>
|
|
118
|
-
{getTeamShortName(match, "b")}
|
|
119
|
-
</span>
|
|
120
|
-
</div>
|
|
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" }}
|
|
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>
|
|
121
168
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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}
|
|
161
|
-
</div>
|
|
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)" }} />
|
|
173
|
+
<span
|
|
174
|
+
className="text-[8px] truncate max-w-[100px]"
|
|
175
|
+
style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.15)" }}
|
|
176
|
+
>
|
|
177
|
+
{venue}
|
|
178
|
+
</span>
|
|
162
179
|
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
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
|
+
/>
|
|
241
|
+
)}
|
|
163
242
|
</div>
|
|
243
|
+
|
|
244
|
+
{/* Match cards for the day */}
|
|
245
|
+
{day.matches.map((match) => (
|
|
246
|
+
<MatchRow key={match.id} match={match} />
|
|
247
|
+
))}
|
|
164
248
|
</motion.div>
|
|
165
249
|
);
|
|
166
250
|
}
|
|
167
251
|
|
|
252
|
+
/* ── Main calendar component ── */
|
|
253
|
+
|
|
168
254
|
interface MatchCalendarProps {
|
|
169
255
|
tournamentId?: number;
|
|
170
256
|
}
|
|
@@ -173,9 +259,49 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
|
173
259
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
174
260
|
const { matches, isLoading, error } = useTDMatches({
|
|
175
261
|
tournamentId,
|
|
176
|
-
limit:
|
|
262
|
+
limit: 100,
|
|
177
263
|
});
|
|
178
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
|
+
|
|
179
305
|
if (isLoading) {
|
|
180
306
|
return (
|
|
181
307
|
<div className="flex items-center justify-center py-4">
|
|
@@ -189,25 +315,55 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
|
189
315
|
return (
|
|
190
316
|
<div className="relative z-30 w-full mb-2 mt-2">
|
|
191
317
|
{/* Section header */}
|
|
192
|
-
<div className="flex items-center gap-2 px-5 mb-
|
|
318
|
+
<div className="flex items-center gap-2 px-5 mb-1">
|
|
193
319
|
<Calendar size={12} style={{ color: "rgba(34,227,232,0.6)" }} />
|
|
194
320
|
<span className="barlowCondensedSemiBold text-[13px] tracking-wider" style={{ color: "rgba(255,255,255,0.45)" }}>
|
|
195
|
-
|
|
321
|
+
SCHEDULE
|
|
196
322
|
</span>
|
|
197
|
-
|
|
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" />
|
|
198
333
|
<span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
|
|
199
|
-
{matches.length}
|
|
334
|
+
{matches.length} matches
|
|
200
335
|
</span>
|
|
201
336
|
</div>
|
|
202
337
|
|
|
203
|
-
{/*
|
|
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 */}
|
|
204
355
|
<div
|
|
205
356
|
ref={scrollRef}
|
|
206
|
-
className="flex gap-2
|
|
207
|
-
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
|
+
}}
|
|
208
364
|
>
|
|
209
|
-
{
|
|
210
|
-
<
|
|
365
|
+
{days.map((day, i) => (
|
|
366
|
+
<DayColumn key={day.dateKey} day={day} index={i} />
|
|
211
367
|
))}
|
|
212
368
|
</div>
|
|
213
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;
|