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