@devrongx/games 0.4.39 → 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 +136 -250
- package/src/pools/types.ts +1 -0
package/package.json
CHANGED
|
@@ -3,192 +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, Target, Users, Zap } from "lucide-react";
|
|
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
|
-
const
|
|
14
|
-
const
|
|
13
|
+
const MS = { UPCOMING: 1, LIVE: 2, COMPLETED: 5 } as const;
|
|
14
|
+
const O = { fontFamily: "Outfit, sans-serif" };
|
|
15
15
|
|
|
16
|
-
/* ──
|
|
16
|
+
/* ── helpers ── */
|
|
17
17
|
|
|
18
|
-
function
|
|
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"; }
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" }).toUpperCase()}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function formatWeekday(d: Date): string {
|
|
25
|
-
return d.toLocaleString("en-US", { weekday: "short" }).toUpperCase();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function formatTime(d: Date): string {
|
|
29
|
-
return d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isSameDay(a: Date, b: Date): boolean {
|
|
33
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isToday(d: Date): boolean { return isSameDay(d, new Date()); }
|
|
37
|
-
|
|
38
|
-
function isTomorrow(d: Date): boolean {
|
|
39
|
-
const t = new Date();
|
|
40
|
-
t.setDate(t.getDate() + 1);
|
|
41
|
-
return isSameDay(d, t);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getDayLabel(d: Date): string {
|
|
45
|
-
if (isToday(d)) return "TODAY";
|
|
46
|
-
if (isTomorrow(d)) return "TOMORROW";
|
|
47
|
-
return formatWeekday(d);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
51
|
-
const t = team === "a" ? match.team_a : match.team_b;
|
|
52
|
-
return t.short_name || t.name.slice(0, 3).toUpperCase();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getShineOpacity(popularity: number): number {
|
|
56
|
-
if (popularity <= 0) return 0;
|
|
57
|
-
return Math.min(popularity / 10, 1);
|
|
58
|
-
}
|
|
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
|
-
|
|
70
|
-
/* ── Grouping: by date ── */
|
|
71
|
-
|
|
72
|
-
interface IMatchDay {
|
|
73
|
-
dateKey: string;
|
|
74
|
-
date: Date;
|
|
75
|
-
label: string;
|
|
76
|
-
dayMonth: string;
|
|
77
|
-
matches: ITDMatch[];
|
|
78
|
-
}
|
|
31
|
+
interface IMatchDay { dateKey: string; date: Date; label: string; dayMonth: string; matches: ITDMatch[]; }
|
|
79
32
|
|
|
80
33
|
function groupByDate(matches: ITDMatch[]): IMatchDay[] {
|
|
81
34
|
const map = new Map<string, { date: Date; matches: ITDMatch[] }>();
|
|
82
35
|
for (const m of matches) {
|
|
83
36
|
if (!m.scheduled_start_at) continue;
|
|
84
|
-
const d =
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
else { map.set(key, { date: d, matches: [m] }); }
|
|
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] });
|
|
89
41
|
}
|
|
90
42
|
return Array.from(map.entries())
|
|
91
43
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
92
|
-
.map(([dateKey, { date, matches
|
|
93
|
-
dateKey, date, label: getDayLabel(date), dayMonth: formatDayMonth(date), matches: dayMatches,
|
|
94
|
-
}));
|
|
44
|
+
.map(([dateKey, { date, matches }]) => ({ dateKey, date, label: dayLabel(date), dayMonth: fmtDay(date), matches }));
|
|
95
45
|
}
|
|
96
46
|
|
|
97
|
-
/* ──
|
|
47
|
+
/* ── game type config ── */
|
|
98
48
|
|
|
99
|
-
const
|
|
49
|
+
const GT: Record<number, { icon: typeof Target; color: string; label: string }> = {
|
|
100
50
|
[TDGameType.PRE_MATCH_BETS]: { icon: Target, color: "#22E3E8", label: "Pre-Match Bets" },
|
|
101
51
|
[TDGameType.FANTASY_11]: { icon: Users, color: "#9945FF", label: "Fantasy 11" },
|
|
102
52
|
[TDGameType.BALL_SEQUENCE]: { icon: Zap, color: "#FF9945", label: "Ball Sequence" },
|
|
103
53
|
};
|
|
104
54
|
|
|
105
|
-
|
|
55
|
+
/* ── pool chip ── */
|
|
56
|
+
|
|
106
57
|
function PoolChip({ pool, color, onPress }: { pool: ITDPool; color: string; onPress: (p: ITDPool) => void }) {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const free =
|
|
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;
|
|
110
62
|
|
|
111
63
|
return (
|
|
112
64
|
<motion.button
|
|
113
65
|
onClick={() => onPress(pool)}
|
|
114
66
|
whileTap={{ scale: 0.92 }}
|
|
115
|
-
className="flex items-center gap-1
|
|
67
|
+
className="relative flex items-center gap-1 shrink-0 rounded-md overflow-hidden"
|
|
116
68
|
style={{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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,
|
|
120
73
|
}}
|
|
121
74
|
>
|
|
122
|
-
{
|
|
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 && (
|
|
123
82
|
<span className="relative flex h-1 w-1 shrink-0">
|
|
124
83
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-50" style={{ backgroundColor: color }} />
|
|
125
84
|
<span className="relative inline-flex rounded-full h-1 w-1" style={{ backgroundColor: color }} />
|
|
126
85
|
</span>
|
|
127
86
|
)}
|
|
128
|
-
|
|
87
|
+
|
|
88
|
+
{/* Price label */}
|
|
89
|
+
<span className="text-[9px] font-semibold leading-none" style={{ ...O, color: free ? "rgba(255,255,255,0.7)" : "#fff" }}>
|
|
129
90
|
{free ? "Free" : pool.display_price}
|
|
130
91
|
</span>
|
|
92
|
+
|
|
93
|
+
{/* Entry count */}
|
|
131
94
|
{pool.entry_count > 0 && (
|
|
132
|
-
<span className="text-[7px]" style={{ ...
|
|
133
|
-
{
|
|
95
|
+
<span className="text-[7px] leading-none" style={{ ...O, color: `${color}80` }}>
|
|
96
|
+
{hCount(pool.entry_count)}
|
|
134
97
|
</span>
|
|
135
98
|
)}
|
|
136
99
|
</motion.button>
|
|
137
100
|
);
|
|
138
101
|
}
|
|
139
102
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}: {
|
|
143
|
-
matchId: number;
|
|
144
|
-
isCompleted: boolean;
|
|
145
|
-
onPoolPress?: (pool: ITDPool) => void;
|
|
146
|
-
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;
|
|
147
107
|
}) {
|
|
148
108
|
const { pools, loading } = useTDPools(matchId, partnerSource);
|
|
149
109
|
if (isCompleted || loading || pools.length === 0) return null;
|
|
150
110
|
|
|
111
|
+
// Group by game_type
|
|
151
112
|
const grouped: Record<number, ITDPool[]> = {};
|
|
152
|
-
for (const p of pools)
|
|
113
|
+
for (const p of pools) (grouped[p.game_type] ??= []).push(p);
|
|
153
114
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
.filter((gt) => gt in GAME_TYPE_STYLE);
|
|
157
|
-
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;
|
|
158
117
|
|
|
159
118
|
return (
|
|
160
|
-
<div className="flex flex-col gap-1.5 mt-1
|
|
161
|
-
{
|
|
162
|
-
const
|
|
163
|
-
|
|
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
|
|
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.
|
|
172
123
|
const seen = new Set<string>();
|
|
173
|
-
const deduped =
|
|
174
|
-
const
|
|
175
|
-
if (seen.has(
|
|
176
|
-
seen.add(
|
|
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);
|
|
177
128
|
return true;
|
|
178
129
|
});
|
|
179
130
|
|
|
180
131
|
return (
|
|
181
|
-
<div key={
|
|
182
|
-
<div className="flex items-center gap-1">
|
|
183
|
-
<Icon size={8} style={{ color:
|
|
184
|
-
<span className="text-[7px]
|
|
185
|
-
{style.label}
|
|
186
|
-
</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>
|
|
187
136
|
</div>
|
|
188
137
|
<div className="flex gap-1 flex-wrap">
|
|
189
|
-
{deduped.map((
|
|
190
|
-
<PoolChip key={pool.id} pool={pool} color={style.color} onPress={onPoolPress ?? (() => {})} />
|
|
191
|
-
))}
|
|
138
|
+
{deduped.map(p => <PoolChip key={p.id} pool={p} color={s.color} onPress={onPoolPress ?? (() => {})} />)}
|
|
192
139
|
</div>
|
|
193
140
|
</div>
|
|
194
141
|
);
|
|
@@ -197,135 +144,92 @@ function MatchGameSection({
|
|
|
197
144
|
);
|
|
198
145
|
}
|
|
199
146
|
|
|
200
|
-
/* ──
|
|
147
|
+
/* ── match card ── */
|
|
201
148
|
|
|
202
|
-
function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
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)) : "";
|
|
206
153
|
const venue = match.venue?.city?.trim() || match.venue?.name;
|
|
207
|
-
const
|
|
208
|
-
const
|
|
154
|
+
const so = shineOp(match.rating_popularity);
|
|
155
|
+
const shine = so > 0 && !done;
|
|
209
156
|
|
|
210
157
|
return (
|
|
211
158
|
<div className="relative w-full rounded-xl p-[1px] overflow-hidden">
|
|
212
|
-
{
|
|
213
|
-
<div
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
animation: `borderShine ${6 - shineOpacity * 2}s linear infinite`,
|
|
220
|
-
}}
|
|
221
|
-
/>
|
|
222
|
-
)}
|
|
223
|
-
{!showShine && (
|
|
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)" }} />
|
|
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
|
+
}} />
|
|
225
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)" }} />}
|
|
226
168
|
|
|
227
|
-
<
|
|
169
|
+
<div
|
|
228
170
|
className="relative flex flex-col gap-1 rounded-xl px-2.5 py-2"
|
|
229
|
-
style={{
|
|
230
|
-
background: isLive
|
|
231
|
-
? "linear-gradient(160deg, rgba(14,10,24,0.98) 0%, rgba(10,8,20,0.98) 100%)"
|
|
232
|
-
: "rgba(10,10,18,0.98)",
|
|
233
|
-
}}
|
|
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)" }}
|
|
234
172
|
>
|
|
235
|
-
{/*
|
|
173
|
+
{/* time */}
|
|
236
174
|
<div className="flex items-center justify-between">
|
|
237
|
-
<span className="text-[9px] font-
|
|
238
|
-
|
|
239
|
-
</span>
|
|
240
|
-
{isLive && (
|
|
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 && (
|
|
241
177
|
<div className="flex items-center gap-1">
|
|
242
|
-
<span className="relative flex h-1.5 w-1.5">
|
|
243
|
-
|
|
244
|
-
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-[#f83cc5]" />
|
|
245
|
-
</span>
|
|
246
|
-
<span className="text-[7px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>LIVE</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>
|
|
247
180
|
</div>
|
|
248
181
|
)}
|
|
249
|
-
{
|
|
250
|
-
<span className="text-[7px]" style={{ ...OUTFIT, color: "rgba(255,255,255,0.45)" }}>
|
|
251
|
-
{match.winner.short_name || match.winner.name} won
|
|
252
|
-
</span>
|
|
253
|
-
)}
|
|
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>}
|
|
254
183
|
</div>
|
|
255
184
|
|
|
256
|
-
{/*
|
|
257
|
-
<div className="flex items-center gap-1.5 justify-center">
|
|
258
|
-
<span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color:
|
|
259
|
-
|
|
260
|
-
</span>
|
|
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" }}>
|
|
263
|
-
{getTeamShortName(match, "b")}
|
|
264
|
-
</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>
|
|
265
190
|
</div>
|
|
266
191
|
|
|
267
|
-
{/*
|
|
192
|
+
{/* venue */}
|
|
268
193
|
{venue && (
|
|
269
194
|
<div className="flex items-center gap-0.5 justify-center">
|
|
270
|
-
<MapPin size={7} style={{ color: "rgba(255,255,255,0.
|
|
271
|
-
<span className="text-[7px] truncate max-w-[110px]" style={{ ...
|
|
272
|
-
{venue}
|
|
273
|
-
</span>
|
|
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>
|
|
274
197
|
</div>
|
|
275
198
|
)}
|
|
276
199
|
|
|
277
|
-
{/*
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
isCompleted={isCompleted}
|
|
281
|
-
onPoolPress={onPoolPress ? (pool) => onPoolPress(pool, match) : undefined}
|
|
282
|
-
partnerSource={partnerSource}
|
|
283
|
-
/>
|
|
284
|
-
</motion.div>
|
|
200
|
+
{/* pools */}
|
|
201
|
+
<MatchPools matchId={match.id} isCompleted={done} onPoolPress={onPoolPress ? p => onPoolPress(p, match) : undefined} partnerSource={partnerSource} />
|
|
202
|
+
</div>
|
|
285
203
|
</div>
|
|
286
204
|
);
|
|
287
205
|
}
|
|
288
206
|
|
|
289
|
-
/* ──
|
|
207
|
+
/* ── day column ── */
|
|
290
208
|
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
const hasLive = day.matches.some(
|
|
294
|
-
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);
|
|
295
213
|
|
|
296
214
|
return (
|
|
297
215
|
<motion.div
|
|
298
|
-
initial={{ opacity: 0, y:
|
|
216
|
+
initial={{ opacity: 0, y: 8 }}
|
|
299
217
|
animate={{ opacity: 1, y: 0 }}
|
|
300
|
-
transition={{ duration: 0.
|
|
218
|
+
transition={{ duration: 0.3, delay: idx * 0.03 }}
|
|
301
219
|
className="shrink-0 flex flex-col gap-1.5"
|
|
302
220
|
style={{ scrollSnapAlign: "start", width: "150px" }}
|
|
303
221
|
>
|
|
304
|
-
<div className="flex flex-col items-center
|
|
305
|
-
<span
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
>
|
|
309
|
-
{day.label}
|
|
310
|
-
</span>
|
|
311
|
-
<span
|
|
312
|
-
className="text-[11px] font-semibold"
|
|
313
|
-
style={{ ...OUTFIT, color: hasLive ? "#f83cc5" : hasToday ? "#22E3E8" : allCompleted ? "rgba(255,255,255,0.3)" : "#fff" }}
|
|
314
|
-
>
|
|
315
|
-
{day.dayMonth}
|
|
316
|
-
</span>
|
|
317
|
-
{(hasToday || hasLive) && (
|
|
318
|
-
<div className="w-4 h-[1.5px] rounded-full mt-0.5" style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }} />
|
|
319
|
-
)}
|
|
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" }} />}
|
|
320
226
|
</div>
|
|
321
|
-
{day.matches.map(
|
|
322
|
-
<MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
323
|
-
))}
|
|
227
|
+
{day.matches.map(m => <MatchCard key={m.id} match={m} onPoolPress={onPoolPress} partnerSource={ps} />)}
|
|
324
228
|
</motion.div>
|
|
325
229
|
);
|
|
326
230
|
}
|
|
327
231
|
|
|
328
|
-
/* ──
|
|
232
|
+
/* ── main ── */
|
|
329
233
|
|
|
330
234
|
interface MatchCalendarProps {
|
|
331
235
|
tournamentId?: number;
|
|
@@ -334,53 +238,41 @@ interface MatchCalendarProps {
|
|
|
334
238
|
}
|
|
335
239
|
|
|
336
240
|
export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
|
|
337
|
-
const
|
|
241
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
338
242
|
const { matches, isLoading, error } = useTDMatches({ tournamentId, limit: 100 });
|
|
339
243
|
const days = useMemo(() => groupByDate(matches), [matches]);
|
|
340
244
|
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
245
|
+
const scrollIdx = useMemo(() => {
|
|
246
|
+
const ti = days.findIndex(d => today(d.date));
|
|
247
|
+
if (ti >= 0) return ti;
|
|
344
248
|
const now = new Date();
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
return 0;
|
|
249
|
+
const ui = days.findIndex(d => d.date >= now);
|
|
250
|
+
return ui >= 0 ? Math.max(0, ui) : 0;
|
|
348
251
|
}, [days]);
|
|
349
252
|
|
|
350
253
|
useEffect(() => {
|
|
351
|
-
if (
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
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" });
|
|
354
257
|
}
|
|
355
|
-
}, [
|
|
258
|
+
}, [scrollIdx, days]);
|
|
356
259
|
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
if (isLoading) {
|
|
360
|
-
return (
|
|
361
|
-
<div className="flex items-center justify-center py-6">
|
|
362
|
-
<Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.3)" }} />
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
365
|
-
}
|
|
260
|
+
const tourney = matches[0]?.tournament;
|
|
366
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>;
|
|
367
263
|
if (error || matches.length === 0) return null;
|
|
368
264
|
|
|
369
265
|
return (
|
|
370
266
|
<div className="relative z-30 w-full mt-5 mb-3">
|
|
371
|
-
{
|
|
267
|
+
{tourney && (
|
|
372
268
|
<div className="px-5 mb-2.5">
|
|
373
269
|
<div className="flex items-center gap-2 mb-0.5">
|
|
374
270
|
<Trophy size={13} style={{ color: "#22E3E8" }} />
|
|
375
|
-
<span className="barlowcondensedBold text-[15px] tracking-wide text-white">{
|
|
271
|
+
<span className="barlowcondensedBold text-[15px] tracking-wide text-white">{tourney.name}</span>
|
|
376
272
|
</div>
|
|
377
273
|
<div className="flex items-center gap-3">
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
{formatDayMonth(toLocalDate(tournament.start_date))} – {formatDayMonth(toLocalDate(tournament.end_date))}
|
|
381
|
-
</span>
|
|
382
|
-
)}
|
|
383
|
-
<span className="text-[10px] text-white/50" style={OUTFIT}>{matches.length} matches</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>
|
|
384
276
|
</div>
|
|
385
277
|
</div>
|
|
386
278
|
)}
|
|
@@ -388,17 +280,11 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
|
|
|
388
280
|
<div className="flex items-center gap-2 px-5 mb-2.5">
|
|
389
281
|
<Calendar size={12} style={{ color: "#22E3E8" }} />
|
|
390
282
|
<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.
|
|
283
|
+
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
392
284
|
</div>
|
|
393
285
|
|
|
394
|
-
<div
|
|
395
|
-
|
|
396
|
-
className="flex gap-2.5 overflow-x-auto px-5 pb-3 scrollbar-hide"
|
|
397
|
-
style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
|
|
398
|
-
>
|
|
399
|
-
{days.map((day, i) => (
|
|
400
|
-
<DayColumn key={day.dateKey} day={day} index={i} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
401
|
-
))}
|
|
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} />)}
|
|
402
288
|
</div>
|
|
403
289
|
</div>
|
|
404
290
|
);
|