@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 +1 -1
- package/src/matches/MatchCalendar.tsx +87 -182
package/package.json
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
97
|
+
/* ── Pool display ── */
|
|
119
98
|
|
|
120
|
-
const GAME_TYPE_STYLE: Record<number, { icon: typeof Target; color: string;
|
|
121
|
-
[TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8",
|
|
122
|
-
[TDGameType.FANTASY_11]: { icon: Users, color: "#9945FF",
|
|
123
|
-
[TDGameType.BALL_SEQUENCE]: { icon: Zap, color: "#FF9945",
|
|
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
|
-
|
|
127
|
-
|
|
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.
|
|
136
|
-
className="flex items-center gap-1
|
|
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 ?
|
|
139
|
-
border: `1px solid ${
|
|
140
|
-
opacity: isClosed ? 0.
|
|
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
|
|
146
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-
|
|
147
|
-
<span className="relative inline-flex rounded-full h-1
|
|
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
|
-
|
|
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-[
|
|
159
|
-
{humanizeCount(pool.entry_count)}
|
|
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-
|
|
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
|
|
164
|
+
// Sort: free first, then by display_price
|
|
198
165
|
const sorted = [...grouped[gt]].sort((a, b) => {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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
|
|
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
|
|
282
|
-
<div className="flex items-center justify-between
|
|
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-[
|
|
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-[
|
|
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-
|
|
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-[
|
|
319
|
-
|
|
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-
|
|
332
|
-
<MapPin size={
|
|
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
|
-
{/*
|
|
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-
|
|
367
|
-
style={{ scrollSnapAlign: "start", width: "
|
|
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-[
|
|
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-[
|
|
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
|
|
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
|
|
436
|
-
|
|
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-
|
|
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-
|
|
460
|
-
<div className="flex items-center gap-2 mb-
|
|
461
|
-
<Trophy size={
|
|
462
|
-
<span className="barlowcondensedBold text-[
|
|
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-[
|
|
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-[
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
<
|
|
482
|
-
<
|
|
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-
|
|
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) => (
|