@devrongx/games 0.4.38 → 0.4.40
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 +159 -368
- package/src/pools/types.ts +1 -0
package/package.json
CHANGED
|
@@ -3,226 +3,139 @@
|
|
|
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, Check } 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
|
|
15
|
-
UPCOMING: 1,
|
|
16
|
-
LIVE: 2,
|
|
17
|
-
COMPLETED: 5,
|
|
18
|
-
} as const;
|
|
13
|
+
const MS = { UPCOMING: 1, LIVE: 2, COMPLETED: 5 } as const;
|
|
14
|
+
const O = { fontFamily: "Outfit, sans-serif" };
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
/* ── helpers ── */
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
function toLocal(iso: string) { return new Date(iso); }
|
|
19
|
+
function fmtDay(d: Date) { return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`; }
|
|
20
|
+
function fmtWeekday(d: Date) { return d.toLocaleString("en-US", { weekday: "short" }).toUpperCase(); }
|
|
21
|
+
function fmtTime(d: Date) { return d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }); }
|
|
22
|
+
function sameDay(a: Date, b: Date) { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); }
|
|
23
|
+
function today(d: Date) { return sameDay(d, new Date()); }
|
|
24
|
+
function tomorrow(d: Date) { const t = new Date(); t.setDate(t.getDate() + 1); return sameDay(d, t); }
|
|
25
|
+
function dayLabel(d: Date) { return today(d) ? "TODAY" : tomorrow(d) ? "TOMORROW" : fmtWeekday(d); }
|
|
26
|
+
function teamShort(m: ITDMatch, t: "a" | "b") { const x = t === "a" ? m.team_a : m.team_b; return x.short_name || x.name.slice(0, 3).toUpperCase(); }
|
|
27
|
+
function shineOp(p: number) { return p <= 0 ? 0 : Math.min(p / 10, 1); }
|
|
28
|
+
function hCount(n: number) { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
|
|
29
|
+
function isFree(p: ITDPool) { const d = (p.display_price || "").toLowerCase().trim(); return d === "" || d === "free"; }
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
return new Date(iso);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function formatDayMonth(d: Date): string {
|
|
29
|
-
return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatWeekday(d: Date): string {
|
|
33
|
-
return d.toLocaleString("en-US", { weekday: "short" }).toUpperCase();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatTime(d: Date): string {
|
|
37
|
-
return d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isSameDay(a: Date, b: Date): boolean {
|
|
41
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isToday(d: Date): boolean {
|
|
45
|
-
return isSameDay(d, new Date());
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isTomorrow(d: Date): boolean {
|
|
49
|
-
const t = new Date();
|
|
50
|
-
t.setDate(t.getDate() + 1);
|
|
51
|
-
return isSameDay(d, t);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function getDayLabel(d: Date): string {
|
|
55
|
-
if (isToday(d)) return "TODAY";
|
|
56
|
-
if (isTomorrow(d)) return "TOMORROW";
|
|
57
|
-
return formatWeekday(d);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
61
|
-
const t = team === "a" ? match.team_a : match.team_b;
|
|
62
|
-
return t.short_name || t.name.slice(0, 3).toUpperCase();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Map popularity 0-10 to border shine opacity (0 = no shine, 10 = full intensity) */
|
|
66
|
-
function getShineOpacity(popularity: number): number {
|
|
67
|
-
if (popularity <= 0) return 0;
|
|
68
|
-
return Math.min(popularity / 10, 1);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function humanizeCount(n: number): string {
|
|
72
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
73
|
-
return String(n);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Determine if a pool is free based on display_price and entry_fee */
|
|
77
|
-
function isFreePool(pool: ITDPool): boolean {
|
|
78
|
-
const dp = (pool.display_price || "").toLowerCase().trim();
|
|
79
|
-
return dp === "" || dp === "free" || (pool.entry_fee === 0 && dp === "");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/* ── Grouping: by date ── */
|
|
83
|
-
|
|
84
|
-
interface IMatchDay {
|
|
85
|
-
dateKey: string;
|
|
86
|
-
date: Date;
|
|
87
|
-
label: string;
|
|
88
|
-
dayMonth: string;
|
|
89
|
-
matches: ITDMatch[];
|
|
90
|
-
}
|
|
31
|
+
interface IMatchDay { dateKey: string; date: Date; label: string; dayMonth: string; matches: ITDMatch[]; }
|
|
91
32
|
|
|
92
33
|
function groupByDate(matches: ITDMatch[]): IMatchDay[] {
|
|
93
34
|
const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
|
|
94
|
-
|
|
95
35
|
for (const m of matches) {
|
|
96
36
|
if (!m.scheduled_start_at) continue;
|
|
97
|
-
const d =
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
existing.matches.push(m);
|
|
102
|
-
} else {
|
|
103
|
-
map.set(key, { date: d, matches: [m] });
|
|
104
|
-
}
|
|
37
|
+
const d = toLocal(m.scheduled_start_at);
|
|
38
|
+
const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
39
|
+
const e = map.get(k);
|
|
40
|
+
if (e) e.matches.push(m); else map.set(k, { date: d, matches: [m] });
|
|
105
41
|
}
|
|
106
|
-
|
|
107
42
|
return Array.from(map.entries())
|
|
108
43
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
109
|
-
.map(([dateKey, { date, matches
|
|
110
|
-
dateKey,
|
|
111
|
-
date,
|
|
112
|
-
label: getDayLabel(date),
|
|
113
|
-
dayMonth: formatDayMonth(date),
|
|
114
|
-
matches: dayMatches,
|
|
115
|
-
}));
|
|
44
|
+
.map(([dateKey, { date, matches }]) => ({ dateKey, date, label: dayLabel(date), dayMonth: fmtDay(date), matches }));
|
|
116
45
|
}
|
|
117
46
|
|
|
118
|
-
/* ──
|
|
47
|
+
/* ── game type config ── */
|
|
119
48
|
|
|
120
|
-
const
|
|
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",
|
|
49
|
+
const GT: Record<number, { icon: typeof Target; color: string; label: string }> = {
|
|
50
|
+
[TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8", label: "Pre-Match Bets" },
|
|
51
|
+
[TDGameType.FANTASY_11]: { icon: Users, color: "#9945FF", label: "Fantasy 11" },
|
|
52
|
+
[TDGameType.BALL_SEQUENCE]: { icon: Zap, color: "#FF9945", label: "Ball Sequence" },
|
|
124
53
|
};
|
|
125
54
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const
|
|
55
|
+
/* ── pool chip ── */
|
|
56
|
+
|
|
57
|
+
function PoolChip({ pool, color, onPress }: { pool: ITDPool; color: string; onPress: (p: ITDPool) => void }) {
|
|
58
|
+
const closed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
|
|
59
|
+
const open = pool.status === TDPoolStatus.OPEN;
|
|
60
|
+
const free = isFree(pool);
|
|
61
|
+
const joined = pool.has_entry === true;
|
|
131
62
|
|
|
132
63
|
return (
|
|
133
64
|
<motion.button
|
|
134
65
|
onClick={() => onPress(pool)}
|
|
135
|
-
whileTap={{ scale: 0.
|
|
136
|
-
className="flex items-center gap-1
|
|
66
|
+
whileTap={{ scale: 0.92 }}
|
|
67
|
+
className="relative flex items-center gap-1 shrink-0 rounded-md overflow-hidden"
|
|
137
68
|
style={{
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
69
|
+
padding: "4px 8px",
|
|
70
|
+
background: joined ? `${color}22` : `${color}0a`,
|
|
71
|
+
border: `1px solid ${joined ? `${color}60` : `${color}${open ? "30" : "18"}`}`,
|
|
72
|
+
opacity: closed ? 0.4 : 1,
|
|
141
73
|
}}
|
|
142
74
|
>
|
|
143
|
-
{/*
|
|
144
|
-
{
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
75
|
+
{/* Joined checkmark */}
|
|
76
|
+
{joined && (
|
|
77
|
+
<Check size={8} strokeWidth={3} style={{ color }} className="shrink-0" />
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Live dot (only if open and not joined) */}
|
|
81
|
+
{open && !joined && (
|
|
82
|
+
<span className="relative flex h-1 w-1 shrink-0">
|
|
83
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-50" style={{ backgroundColor: color }} />
|
|
84
|
+
<span className="relative inline-flex rounded-full h-1 w-1" style={{ backgroundColor: color }} />
|
|
148
85
|
</span>
|
|
149
86
|
)}
|
|
150
87
|
|
|
151
|
-
{/*
|
|
152
|
-
<span className="
|
|
153
|
-
{free ? "
|
|
88
|
+
{/* Price label */}
|
|
89
|
+
<span className="text-[9px] font-semibold leading-none" style={{ ...O, color: free ? "rgba(255,255,255,0.7)" : "#fff" }}>
|
|
90
|
+
{free ? "Free" : pool.display_price}
|
|
154
91
|
</span>
|
|
155
92
|
|
|
156
|
-
{/* Entry count
|
|
93
|
+
{/* Entry count */}
|
|
157
94
|
{pool.entry_count > 0 && (
|
|
158
|
-
<span className="text-[
|
|
159
|
-
{
|
|
95
|
+
<span className="text-[7px] leading-none" style={{ ...O, color: `${color}80` }}>
|
|
96
|
+
{hCount(pool.entry_count)}
|
|
160
97
|
</span>
|
|
161
98
|
)}
|
|
162
99
|
</motion.button>
|
|
163
100
|
);
|
|
164
101
|
}
|
|
165
102
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
onPoolPress
|
|
170
|
-
partnerSource,
|
|
171
|
-
}: {
|
|
172
|
-
matchId: number;
|
|
173
|
-
isCompleted: boolean;
|
|
174
|
-
onPoolPress?: (pool: ITDPool) => void;
|
|
175
|
-
partnerSource?: string;
|
|
103
|
+
/* ── pools section inside a match card ── */
|
|
104
|
+
|
|
105
|
+
function MatchPools({ matchId, isCompleted, onPoolPress, partnerSource }: {
|
|
106
|
+
matchId: number; isCompleted: boolean; onPoolPress?: (p: ITDPool) => void; partnerSource?: string;
|
|
176
107
|
}) {
|
|
177
108
|
const { pools, loading } = useTDPools(matchId, partnerSource);
|
|
178
|
-
|
|
179
109
|
if (isCompleted || loading || pools.length === 0) return null;
|
|
180
110
|
|
|
181
|
-
// Group by game_type
|
|
111
|
+
// Group by game_type
|
|
182
112
|
const grouped: Record<number, ITDPool[]> = {};
|
|
183
|
-
for (const p of pools)
|
|
184
|
-
(grouped[p.game_type] ??= []).push(p);
|
|
185
|
-
}
|
|
113
|
+
for (const p of pools) (grouped[p.game_type] ??= []).push(p);
|
|
186
114
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
.filter((gt) => gt in GAME_TYPE_STYLE);
|
|
190
|
-
if (gameTypes.length === 0) return null;
|
|
115
|
+
const types = (Object.keys(grouped) as unknown as number[]).map(Number).filter(t => t in GT);
|
|
116
|
+
if (types.length === 0) return null;
|
|
191
117
|
|
|
192
118
|
return (
|
|
193
|
-
<div className="flex flex-col gap-
|
|
194
|
-
{
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return
|
|
119
|
+
<div className="flex flex-col gap-1.5 mt-1 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
|
120
|
+
{types.map(t => {
|
|
121
|
+
const s = GT[t]; const Icon = s.icon;
|
|
122
|
+
// Deduplicate: one free, then paid by price. Backend already sorts correctly.
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
const deduped = grouped[t].filter(p => {
|
|
125
|
+
const k = isFree(p) ? "free" : p.display_price;
|
|
126
|
+
if (seen.has(k)) return false;
|
|
127
|
+
seen.add(k);
|
|
128
|
+
return true;
|
|
203
129
|
});
|
|
204
130
|
|
|
205
131
|
return (
|
|
206
|
-
<div key={
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
<span className="text-[8px] barlowcondensedBold tracking-wide" style={{ color: style.color }}>
|
|
211
|
-
{style.label}
|
|
212
|
-
</span>
|
|
132
|
+
<div key={t}>
|
|
133
|
+
<div className="flex items-center gap-1 mb-1">
|
|
134
|
+
<Icon size={8} style={{ color: s.color }} />
|
|
135
|
+
<span className="text-[7px] font-bold tracking-wider uppercase" style={{ ...O, color: `${s.color}bb` }}>{s.label}</span>
|
|
213
136
|
</div>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{sorted.map((pool, idx) => (
|
|
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>
|
|
225
|
-
))}
|
|
137
|
+
<div className="flex gap-1 flex-wrap">
|
|
138
|
+
{deduped.map(p => <PoolChip key={p.id} pool={p} color={s.color} onPress={onPoolPress ?? (() => {})} />)}
|
|
226
139
|
</div>
|
|
227
140
|
</div>
|
|
228
141
|
);
|
|
@@ -231,269 +144,147 @@ function MatchGameSection({
|
|
|
231
144
|
);
|
|
232
145
|
}
|
|
233
146
|
|
|
234
|
-
/* ──
|
|
147
|
+
/* ── match card ── */
|
|
235
148
|
|
|
236
|
-
function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const time = match.scheduled_start_at ?
|
|
149
|
+
function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (p: ITDPool, m: ITDMatch) => void; partnerSource?: string }) {
|
|
150
|
+
const live = match.status === MS.LIVE;
|
|
151
|
+
const done = match.status === MS.COMPLETED;
|
|
152
|
+
const time = match.scheduled_start_at ? fmtTime(toLocal(match.scheduled_start_at)) : "";
|
|
240
153
|
const venue = match.venue?.city?.trim() || match.venue?.name;
|
|
241
|
-
const
|
|
242
|
-
const
|
|
154
|
+
const so = shineOp(match.rating_popularity);
|
|
155
|
+
const shine = so > 0 && !done;
|
|
243
156
|
|
|
244
157
|
return (
|
|
245
158
|
<div className="relative w-full rounded-xl p-[1px] overflow-hidden">
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
: `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%)`,
|
|
254
|
-
animation: `borderShine ${6 - shineOpacity * 2}s linear infinite`,
|
|
255
|
-
}}
|
|
256
|
-
/>
|
|
159
|
+
{shine && (
|
|
160
|
+
<div className="absolute inset-[-50%] pointer-events-none" style={{
|
|
161
|
+
background: live
|
|
162
|
+
? `conic-gradient(from 0deg, transparent 0%, transparent 65%, rgba(248,60,197,${so * 0.6}) 75%, rgba(255,255,255,${so * 0.7}) 80%, rgba(248,60,197,${so * 0.6}) 85%, transparent 95%, transparent 100%)`
|
|
163
|
+
: `conic-gradient(from 0deg, transparent 0%, transparent 70%, rgba(34,227,232,${so * 0.5}) 78%, rgba(255,255,255,${so * 0.6}) 80%, rgba(34,227,232,${so * 0.5}) 82%, transparent 90%, transparent 100%)`,
|
|
164
|
+
animation: `borderShine ${6 - so * 2}s linear infinite`,
|
|
165
|
+
}} />
|
|
257
166
|
)}
|
|
167
|
+
{!shine && <div className="absolute inset-0 rounded-xl" style={{ border: done ? "1px solid rgba(255,255,255,0.05)" : "1px solid rgba(255,255,255,0.08)" }} />}
|
|
258
168
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
/>
|
|
269
|
-
)}
|
|
270
|
-
|
|
271
|
-
<motion.div
|
|
272
|
-
className="relative flex flex-col gap-1.5 rounded-xl px-3 py-2.5"
|
|
273
|
-
style={{
|
|
274
|
-
background: isLive
|
|
275
|
-
? "linear-gradient(160deg, rgba(14,10,24,0.98) 0%, rgba(10,8,20,0.98) 100%)"
|
|
276
|
-
: "rgba(10,10,18,0.98)",
|
|
277
|
-
}}
|
|
278
|
-
whileHover={{ backgroundColor: "rgba(18,18,28,0.98)" }}
|
|
279
|
-
transition={{ duration: 0.2 }}
|
|
169
|
+
<div
|
|
170
|
+
className="relative flex flex-col gap-1 rounded-xl px-2.5 py-2"
|
|
171
|
+
style={{ background: live ? "linear-gradient(160deg, rgba(14,10,24,0.98), rgba(10,8,20,0.98))" : "rgba(10,10,18,0.98)" }}
|
|
280
172
|
>
|
|
281
|
-
{/*
|
|
282
|
-
<div className="flex items-center justify-between
|
|
283
|
-
<span
|
|
284
|
-
|
|
285
|
-
style={{
|
|
286
|
-
...OUTFIT,
|
|
287
|
-
color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.45)" : "rgba(255,255,255,0.7)",
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
{time}
|
|
291
|
-
</span>
|
|
292
|
-
{isLive && (
|
|
173
|
+
{/* time */}
|
|
174
|
+
<div className="flex items-center justify-between">
|
|
175
|
+
<span className="text-[9px] font-medium" style={{ ...O, color: live ? "#f83cc5" : done ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.55)" }}>{time}</span>
|
|
176
|
+
{live && (
|
|
293
177
|
<div className="flex items-center gap-1">
|
|
294
|
-
<span className="relative flex h-1.5 w-1.5">
|
|
295
|
-
|
|
296
|
-
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
|
|
297
|
-
</span>
|
|
298
|
-
<span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
|
|
299
|
-
LIVE
|
|
300
|
-
</span>
|
|
178
|
+
<span className="relative flex h-1.5 w-1.5"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" /><span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" /></span>
|
|
179
|
+
<span className="text-[7px] font-bold tracking-widest" style={{ ...O, color: "#f83cc5" }}>LIVE</span>
|
|
301
180
|
</div>
|
|
302
181
|
)}
|
|
303
|
-
{
|
|
304
|
-
<span className="text-[8px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.5)" }}>
|
|
305
|
-
{match.winner.short_name || match.winner.name} won
|
|
306
|
-
</span>
|
|
307
|
-
)}
|
|
182
|
+
{done && match.winner && <span className="text-[7px]" style={{ ...O, color: "rgba(255,255,255,0.4)" }}>{match.winner.short_name || match.winner.name} won</span>}
|
|
308
183
|
</div>
|
|
309
184
|
|
|
310
|
-
{/*
|
|
311
|
-
<div className="flex items-center gap-
|
|
312
|
-
<span
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
>
|
|
316
|
-
{getTeamShortName(match, "a")}
|
|
317
|
-
</span>
|
|
318
|
-
<span className="text-[9px] font-semibold" style={{ ...OUTFIT, color: "rgba(255,255,255,0.45)" }}>
|
|
319
|
-
vs
|
|
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
|
-
>
|
|
325
|
-
{getTeamShortName(match, "b")}
|
|
326
|
-
</span>
|
|
185
|
+
{/* teams */}
|
|
186
|
+
<div className="flex items-center gap-1.5 justify-center py-0.5">
|
|
187
|
+
<span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color: done ? "rgba(255,255,255,0.35)" : "#fff" }}>{teamShort(match, "a")}</span>
|
|
188
|
+
<span className="text-[8px] font-medium" style={{ ...O, color: "rgba(255,255,255,0.35)" }}>vs</span>
|
|
189
|
+
<span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color: done ? "rgba(255,255,255,0.35)" : "#fff" }}>{teamShort(match, "b")}</span>
|
|
327
190
|
</div>
|
|
328
191
|
|
|
329
|
-
{/*
|
|
192
|
+
{/* venue */}
|
|
330
193
|
{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
|
-
>
|
|
337
|
-
{venue}
|
|
338
|
-
</span>
|
|
194
|
+
<div className="flex items-center gap-0.5 justify-center">
|
|
195
|
+
<MapPin size={7} style={{ color: "rgba(255,255,255,0.35)" }} />
|
|
196
|
+
<span className="text-[7px] truncate max-w-[110px]" style={{ ...O, color: "rgba(255,255,255,0.5)" }}>{venue}</span>
|
|
339
197
|
</div>
|
|
340
198
|
)}
|
|
341
199
|
|
|
342
|
-
{/*
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
isCompleted={isCompleted}
|
|
346
|
-
onPoolPress={onPoolPress ? (pool) => onPoolPress(pool, match) : undefined}
|
|
347
|
-
partnerSource={partnerSource}
|
|
348
|
-
/>
|
|
349
|
-
</motion.div>
|
|
200
|
+
{/* pools */}
|
|
201
|
+
<MatchPools matchId={match.id} isCompleted={done} onPoolPress={onPoolPress ? p => onPoolPress(p, match) : undefined} partnerSource={partnerSource} />
|
|
202
|
+
</div>
|
|
350
203
|
</div>
|
|
351
204
|
);
|
|
352
205
|
}
|
|
353
206
|
|
|
354
|
-
/* ──
|
|
207
|
+
/* ── day column ── */
|
|
355
208
|
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
const hasLive = day.matches.some(
|
|
359
|
-
const
|
|
209
|
+
function DayCol({ day, idx, onPoolPress, ps }: { day: IMatchDay; idx: number; onPoolPress?: (p: ITDPool, m: ITDMatch) => void; ps?: string }) {
|
|
210
|
+
const isToday = day.label === "TODAY";
|
|
211
|
+
const hasLive = day.matches.some(m => m.status === MS.LIVE);
|
|
212
|
+
const allDone = day.matches.every(m => m.status === MS.COMPLETED);
|
|
360
213
|
|
|
361
214
|
return (
|
|
362
215
|
<motion.div
|
|
363
|
-
initial={{ opacity: 0, y:
|
|
216
|
+
initial={{ opacity: 0, y: 8 }}
|
|
364
217
|
animate={{ opacity: 1, y: 0 }}
|
|
365
|
-
transition={{ duration: 0.
|
|
366
|
-
className="shrink-0 flex flex-col gap-
|
|
367
|
-
style={{ scrollSnapAlign: "start", width: "
|
|
218
|
+
transition={{ duration: 0.3, delay: idx * 0.03 }}
|
|
219
|
+
className="shrink-0 flex flex-col gap-1.5"
|
|
220
|
+
style={{ scrollSnapAlign: "start", width: "150px" }}
|
|
368
221
|
>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
<span
|
|
372
|
-
|
|
373
|
-
style={{
|
|
374
|
-
color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "rgba(255,255,255,0.9)",
|
|
375
|
-
}}
|
|
376
|
-
>
|
|
377
|
-
{day.label}
|
|
378
|
-
</span>
|
|
379
|
-
<span
|
|
380
|
-
className="text-[12px] font-semibold"
|
|
381
|
-
style={{
|
|
382
|
-
...OUTFIT,
|
|
383
|
-
color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.35)" : "#fff",
|
|
384
|
-
}}
|
|
385
|
-
>
|
|
386
|
-
{day.dayMonth}
|
|
387
|
-
</span>
|
|
388
|
-
{(hasToday || hasLive) && (
|
|
389
|
-
<div
|
|
390
|
-
className="w-5 h-[2px] rounded-full mt-0.5"
|
|
391
|
-
style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }}
|
|
392
|
-
/>
|
|
393
|
-
)}
|
|
222
|
+
<div className="flex flex-col items-center pb-1">
|
|
223
|
+
<span className="text-[9px] font-bold tracking-widest" style={{ ...O, color: hasLive ? "#f83cc5" : isToday ? "#22E3E8" : allDone ? "rgba(255,255,255,0.25)" : "rgba(255,255,255,0.8)" }}>{day.label}</span>
|
|
224
|
+
<span className="text-[11px] font-semibold" style={{ ...O, color: hasLive ? "#f83cc5" : isToday ? "#22E3E8" : allDone ? "rgba(255,255,255,0.25)" : "#fff" }}>{day.dayMonth}</span>
|
|
225
|
+
{(isToday || hasLive) && <div className="w-4 h-[1.5px] rounded-full mt-0.5" style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }} />}
|
|
394
226
|
</div>
|
|
395
|
-
|
|
396
|
-
{/* Match cards for the day */}
|
|
397
|
-
{day.matches.map((match) => (
|
|
398
|
-
<MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
399
|
-
))}
|
|
227
|
+
{day.matches.map(m => <MatchCard key={m.id} match={m} onPoolPress={onPoolPress} partnerSource={ps} />)}
|
|
400
228
|
</motion.div>
|
|
401
229
|
);
|
|
402
230
|
}
|
|
403
231
|
|
|
404
|
-
/* ──
|
|
232
|
+
/* ── main ── */
|
|
405
233
|
|
|
406
234
|
interface MatchCalendarProps {
|
|
407
235
|
tournamentId?: number;
|
|
408
|
-
/** Called when user taps a pool pill. Receives both the pool and its parent match. */
|
|
409
236
|
onPoolPress?: (pool: ITDPool, match: ITDMatch) => void;
|
|
410
|
-
/** Filter pools to a specific partner source (e.g. "iamgame") */
|
|
411
237
|
partnerSource?: string;
|
|
412
238
|
}
|
|
413
239
|
|
|
414
240
|
export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
|
|
415
|
-
const
|
|
416
|
-
const { matches, isLoading, error } = useTDMatches({
|
|
417
|
-
tournamentId,
|
|
418
|
-
limit: 100,
|
|
419
|
-
});
|
|
420
|
-
|
|
241
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
242
|
+
const { matches, isLoading, error } = useTDMatches({ tournamentId, limit: 100 });
|
|
421
243
|
const days = useMemo(() => groupByDate(matches), [matches]);
|
|
422
244
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (tIdx >= 0) return tIdx;
|
|
245
|
+
const scrollIdx = useMemo(() => {
|
|
246
|
+
const ti = days.findIndex(d => today(d.date));
|
|
247
|
+
if (ti >= 0) return ti;
|
|
427
248
|
const now = new Date();
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
return 0;
|
|
249
|
+
const ui = days.findIndex(d => d.date >= now);
|
|
250
|
+
return ui >= 0 ? Math.max(0, ui) : 0;
|
|
431
251
|
}, [days]);
|
|
432
252
|
|
|
433
253
|
useEffect(() => {
|
|
434
|
-
if (
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
if (target) {
|
|
438
|
-
container.scrollTo({ left: target.offsetLeft - 20, behavior: "smooth" });
|
|
439
|
-
}
|
|
254
|
+
if (ref.current && scrollIdx > 0) {
|
|
255
|
+
const t = ref.current.children[scrollIdx] as HTMLElement;
|
|
256
|
+
if (t) ref.current.scrollTo({ left: t.offsetLeft - 16, behavior: "smooth" });
|
|
440
257
|
}
|
|
441
|
-
}, [
|
|
258
|
+
}, [scrollIdx, days]);
|
|
442
259
|
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
if (isLoading) {
|
|
446
|
-
return (
|
|
447
|
-
<div className="flex items-center justify-center py-6">
|
|
448
|
-
<Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} />
|
|
449
|
-
</div>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
260
|
+
const tourney = matches[0]?.tournament;
|
|
452
261
|
|
|
262
|
+
if (isLoading) return <div className="flex items-center justify-center py-6"><Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} /></div>;
|
|
453
263
|
if (error || matches.length === 0) return null;
|
|
454
264
|
|
|
455
265
|
return (
|
|
456
|
-
<div className="relative z-30 w-full mt-
|
|
457
|
-
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
<
|
|
462
|
-
<span className="barlowcondensedBold text-[16px] tracking-wide text-white">
|
|
463
|
-
{tournament.name}
|
|
464
|
-
</span>
|
|
266
|
+
<div className="relative z-30 w-full mt-5 mb-3">
|
|
267
|
+
{tourney && (
|
|
268
|
+
<div className="px-5 mb-2.5">
|
|
269
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
270
|
+
<Trophy size={13} style={{ color: "#22E3E8" }} />
|
|
271
|
+
<span className="barlowcondensedBold text-[15px] tracking-wide text-white">{tourney.name}</span>
|
|
465
272
|
</div>
|
|
466
273
|
<div className="flex items-center gap-3">
|
|
467
|
-
{
|
|
468
|
-
|
|
469
|
-
{formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
|
|
470
|
-
</span>
|
|
471
|
-
)}
|
|
472
|
-
<span className="text-[11px] text-white" style={OUTFIT}>
|
|
473
|
-
{matches.length} matches announced
|
|
474
|
-
</span>
|
|
274
|
+
{tourney.start_date && tourney.end_date && <span className="text-[10px] text-white/50" style={O}>{fmtDay(toLocal(tourney.start_date))} – {fmtDay(toLocal(tourney.end_date))}</span>}
|
|
275
|
+
<span className="text-[10px] text-white/50" style={O}>{matches.length} matches</span>
|
|
475
276
|
</div>
|
|
476
277
|
</div>
|
|
477
278
|
)}
|
|
478
279
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
<
|
|
482
|
-
<
|
|
483
|
-
SCHEDULE
|
|
484
|
-
</span>
|
|
485
|
-
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.08)" }} />
|
|
280
|
+
<div className="flex items-center gap-2 px-5 mb-2.5">
|
|
281
|
+
<Calendar size={12} style={{ color: "#22E3E8" }} />
|
|
282
|
+
<span className="barlowCondensedSemiBold text-[13px] tracking-wider text-white">SCHEDULE</span>
|
|
283
|
+
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
486
284
|
</div>
|
|
487
285
|
|
|
488
|
-
{
|
|
489
|
-
|
|
490
|
-
ref={scrollRef}
|
|
491
|
-
className="flex gap-3 overflow-x-auto px-5 pb-4 scrollbar-hide"
|
|
492
|
-
style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
|
|
493
|
-
>
|
|
494
|
-
{days.map((day, i) => (
|
|
495
|
-
<DayColumn key={day.dateKey} day={day} index={i} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
496
|
-
))}
|
|
286
|
+
<div ref={ref} className="flex gap-2.5 overflow-x-auto px-5 pb-3 scrollbar-hide" style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}>
|
|
287
|
+
{days.map((d, i) => <DayCol key={d.dateKey} day={d} idx={i} onPoolPress={onPoolPress} ps={partnerSource} />)}
|
|
497
288
|
</div>
|
|
498
289
|
</div>
|
|
499
290
|
);
|