@devrongx/games 0.4.39 → 0.4.41
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 +152 -250
- package/src/pools/types.ts +1 -0
package/package.json
CHANGED
|
@@ -3,192 +3,155 @@
|
|
|
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
|
+
/* ── USDC icon (inline SVG) ── */
|
|
56
|
+
|
|
57
|
+
function UsdcIcon({ size = 10 }: { size?: number }) {
|
|
58
|
+
return (
|
|
59
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="shrink-0">
|
|
60
|
+
<path d="M12 22C17.5417 22 22 17.5417 22 12C22 6.4583 17.5417 2 12 2C6.4583 2 2 6.4583 2 12C2 17.5417 6.4583 22 12 22Z" fill="#2775CA"/>
|
|
61
|
+
<path d="M14.7493 13.582C14.7493 12.1237 13.8743 11.6237 12.1243 11.4154C10.8743 11.2487 10.6243 10.9154 10.6243 10.332C10.6243 9.74863 11.041 9.37373 11.8743 9.37373C12.6243 9.37373 13.041 9.62373 13.2493 10.2487C13.291 10.3737 13.416 10.457 13.541 10.457H14.2076C14.3743 10.457 14.4993 10.332 14.4993 10.1654V10.1237C14.3326 9.20703 13.5826 8.49873 12.6243 8.41543V7.41543C12.6243 7.24873 12.4993 7.12373 12.291 7.08203H11.666C11.4993 7.08203 11.3743 7.20703 11.3326 7.41543V8.37373C10.0826 8.54043 9.29102 9.37373 9.29102 10.4154C9.29102 11.7904 10.1243 12.332 11.8743 12.5404C13.041 12.7487 13.416 12.9987 13.416 13.6654C13.416 14.3321 12.8326 14.7904 12.041 14.7904C10.9576 14.7904 10.5826 14.332 10.4576 13.707C10.416 13.5404 10.291 13.457 10.166 13.457H9.45762C9.29102 13.457 9.16602 13.582 9.16602 13.7487V13.7904C9.33262 14.832 9.99932 15.582 11.3743 15.7904V16.7904C11.3743 16.957 11.4993 17.082 11.7076 17.1237H12.3326C12.4993 17.1237 12.6243 16.9987 12.666 16.7904V15.7904C13.916 15.582 14.7493 14.707 14.7493 13.582Z" fill="white"/>
|
|
62
|
+
<path d="M9.87581 17.9596C6.62581 16.793 4.95911 13.168 6.16751 9.95957C6.79251 8.20957 8.16751 6.87627 9.87581 6.25127C10.0425 6.16797 10.1258 6.04297 10.1258 5.83457V5.25127C10.1258 5.08457 10.0425 4.95957 9.87581 4.91797C9.83411 4.91797 9.75081 4.91797 9.70911 4.95957C5.75081 6.20957 3.58411 10.418 4.83411 14.3763C5.58411 16.7096 7.37581 18.5013 9.70911 19.2513C9.87581 19.3346 10.0425 19.2513 10.0841 19.0846C10.1258 19.043 10.1258 19.0013 10.1258 18.918V18.3346C10.1258 18.2096 10.0008 18.043 9.87581 17.9596ZM14.2925 4.95957C14.1258 4.87627 13.9591 4.95957 13.9175 5.12627C13.8758 5.16797 13.8758 5.20957 13.8758 5.29297V5.87627C13.8758 6.04297 14.0008 6.20957 14.1258 6.29297C17.3758 7.45957 19.0425 11.0846 17.8341 14.293C17.2091 16.043 15.8341 17.3763 14.1258 18.0013C13.9591 18.0846 13.8758 18.2096 13.8758 18.418V19.0013C13.8758 19.168 13.9591 19.293 14.1258 19.3346C14.1675 19.3346 14.2508 19.3346 14.2925 19.293C18.2508 18.043 20.4175 13.8346 19.1675 9.87627C18.4175 7.50127 16.5841 5.70957 14.2925 4.95957Z" fill="white"/>
|
|
63
|
+
</svg>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── pool chip ── */
|
|
68
|
+
|
|
106
69
|
function PoolChip({ pool, color, onPress }: { pool: ITDPool; color: string; onPress: (p: ITDPool) => void }) {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const free =
|
|
70
|
+
const closed = pool.status === TDPoolStatus.CLOSED || pool.status === TDPoolStatus.RESOLVING || pool.status === TDPoolStatus.COMPLETE;
|
|
71
|
+
const open = pool.status === TDPoolStatus.OPEN;
|
|
72
|
+
const free = isFree(pool);
|
|
73
|
+
const joined = pool.has_entry === true;
|
|
74
|
+
const paid = !free;
|
|
110
75
|
|
|
111
76
|
return (
|
|
112
77
|
<motion.button
|
|
113
78
|
onClick={() => onPress(pool)}
|
|
114
79
|
whileTap={{ scale: 0.92 }}
|
|
115
|
-
className="flex items-center gap-1
|
|
80
|
+
className="relative flex items-center gap-1 shrink-0 rounded-md overflow-hidden"
|
|
116
81
|
style={{
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
82
|
+
padding: "4px 8px",
|
|
83
|
+
background: joined ? `${color}22` : `${color}0a`,
|
|
84
|
+
border: `1px solid ${joined ? `${color}60` : `${color}${open ? "30" : "18"}`}`,
|
|
85
|
+
opacity: closed ? 0.4 : 1,
|
|
120
86
|
}}
|
|
121
87
|
>
|
|
122
|
-
{
|
|
88
|
+
{/* Joined checkmark */}
|
|
89
|
+
{joined && (
|
|
90
|
+
<Check size={8} strokeWidth={3} style={{ color }} className="shrink-0" />
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* USDC icon for paid pools (not joined) */}
|
|
94
|
+
{paid && !joined && <UsdcIcon size={10} />}
|
|
95
|
+
|
|
96
|
+
{/* Live dot for free pools (only if open and not joined) */}
|
|
97
|
+
{free && open && !joined && (
|
|
123
98
|
<span className="relative flex h-1 w-1 shrink-0">
|
|
124
99
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-50" style={{ backgroundColor: color }} />
|
|
125
100
|
<span className="relative inline-flex rounded-full h-1 w-1" style={{ backgroundColor: color }} />
|
|
126
101
|
</span>
|
|
127
102
|
)}
|
|
128
|
-
|
|
103
|
+
|
|
104
|
+
{/* Price label */}
|
|
105
|
+
<span className="text-[9px] font-semibold leading-none" style={{ ...O, color: free ? "rgba(255,255,255,0.7)" : "#fff" }}>
|
|
129
106
|
{free ? "Free" : pool.display_price}
|
|
130
107
|
</span>
|
|
108
|
+
|
|
109
|
+
{/* Entry count */}
|
|
131
110
|
{pool.entry_count > 0 && (
|
|
132
|
-
<span className="text-[7px]" style={{ ...
|
|
133
|
-
{
|
|
111
|
+
<span className="text-[7px] leading-none" style={{ ...O, color: `${color}80` }}>
|
|
112
|
+
{hCount(pool.entry_count)}
|
|
134
113
|
</span>
|
|
135
114
|
)}
|
|
136
115
|
</motion.button>
|
|
137
116
|
);
|
|
138
117
|
}
|
|
139
118
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}: {
|
|
143
|
-
matchId: number;
|
|
144
|
-
isCompleted: boolean;
|
|
145
|
-
onPoolPress?: (pool: ITDPool) => void;
|
|
146
|
-
partnerSource?: string;
|
|
119
|
+
/* ── pools section inside a match card ── */
|
|
120
|
+
|
|
121
|
+
function MatchPools({ matchId, isCompleted, onPoolPress, partnerSource }: {
|
|
122
|
+
matchId: number; isCompleted: boolean; onPoolPress?: (p: ITDPool) => void; partnerSource?: string;
|
|
147
123
|
}) {
|
|
148
124
|
const { pools, loading } = useTDPools(matchId, partnerSource);
|
|
149
125
|
if (isCompleted || loading || pools.length === 0) return null;
|
|
150
126
|
|
|
127
|
+
// Group by game_type
|
|
151
128
|
const grouped: Record<number, ITDPool[]> = {};
|
|
152
|
-
for (const p of pools)
|
|
129
|
+
for (const p of pools) (grouped[p.game_type] ??= []).push(p);
|
|
153
130
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
.filter((gt) => gt in GAME_TYPE_STYLE);
|
|
157
|
-
if (gameTypes.length === 0) return null;
|
|
131
|
+
const types = (Object.keys(grouped) as unknown as number[]).map(Number).filter(t => t in GT);
|
|
132
|
+
if (types.length === 0) return null;
|
|
158
133
|
|
|
159
134
|
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
|
|
135
|
+
<div className="flex flex-col gap-1.5 mt-1 pt-1.5" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
|
136
|
+
{types.map(t => {
|
|
137
|
+
const s = GT[t]; const Icon = s.icon;
|
|
138
|
+
// Deduplicate: one free, then paid by price. Backend already sorts correctly.
|
|
172
139
|
const seen = new Set<string>();
|
|
173
|
-
const deduped =
|
|
174
|
-
const
|
|
175
|
-
if (seen.has(
|
|
176
|
-
seen.add(
|
|
140
|
+
const deduped = grouped[t].filter(p => {
|
|
141
|
+
const k = isFree(p) ? "free" : p.display_price;
|
|
142
|
+
if (seen.has(k)) return false;
|
|
143
|
+
seen.add(k);
|
|
177
144
|
return true;
|
|
178
145
|
});
|
|
179
146
|
|
|
180
147
|
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>
|
|
148
|
+
<div key={t}>
|
|
149
|
+
<div className="flex items-center gap-1 mb-1">
|
|
150
|
+
<Icon size={8} style={{ color: s.color }} />
|
|
151
|
+
<span className="text-[7px] font-bold tracking-wider uppercase" style={{ ...O, color: `${s.color}bb` }}>{s.label}</span>
|
|
187
152
|
</div>
|
|
188
153
|
<div className="flex gap-1 flex-wrap">
|
|
189
|
-
{deduped.map((
|
|
190
|
-
<PoolChip key={pool.id} pool={pool} color={style.color} onPress={onPoolPress ?? (() => {})} />
|
|
191
|
-
))}
|
|
154
|
+
{deduped.map(p => <PoolChip key={p.id} pool={p} color={s.color} onPress={onPoolPress ?? (() => {})} />)}
|
|
192
155
|
</div>
|
|
193
156
|
</div>
|
|
194
157
|
);
|
|
@@ -197,135 +160,92 @@ function MatchGameSection({
|
|
|
197
160
|
);
|
|
198
161
|
}
|
|
199
162
|
|
|
200
|
-
/* ──
|
|
163
|
+
/* ── match card ── */
|
|
201
164
|
|
|
202
|
-
function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const time = match.scheduled_start_at ?
|
|
165
|
+
function MatchCard({ match, onPoolPress, partnerSource }: { match: ITDMatch; onPoolPress?: (p: ITDPool, m: ITDMatch) => void; partnerSource?: string }) {
|
|
166
|
+
const live = match.status === MS.LIVE;
|
|
167
|
+
const done = match.status === MS.COMPLETED;
|
|
168
|
+
const time = match.scheduled_start_at ? fmtTime(toLocal(match.scheduled_start_at)) : "";
|
|
206
169
|
const venue = match.venue?.city?.trim() || match.venue?.name;
|
|
207
|
-
const
|
|
208
|
-
const
|
|
170
|
+
const so = shineOp(match.rating_popularity);
|
|
171
|
+
const shine = so > 0 && !done;
|
|
209
172
|
|
|
210
173
|
return (
|
|
211
174
|
<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)" }} />
|
|
175
|
+
{shine && (
|
|
176
|
+
<div className="absolute inset-[-50%] pointer-events-none" style={{
|
|
177
|
+
background: live
|
|
178
|
+
? `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%)`
|
|
179
|
+
: `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%)`,
|
|
180
|
+
animation: `borderShine ${6 - so * 2}s linear infinite`,
|
|
181
|
+
}} />
|
|
225
182
|
)}
|
|
183
|
+
{!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
184
|
|
|
227
|
-
<
|
|
185
|
+
<div
|
|
228
186
|
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
|
-
}}
|
|
187
|
+
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
188
|
>
|
|
235
|
-
{/*
|
|
189
|
+
{/* time */}
|
|
236
190
|
<div className="flex items-center justify-between">
|
|
237
|
-
<span className="text-[9px] font-
|
|
238
|
-
|
|
239
|
-
</span>
|
|
240
|
-
{isLive && (
|
|
191
|
+
<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>
|
|
192
|
+
{live && (
|
|
241
193
|
<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>
|
|
194
|
+
<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>
|
|
195
|
+
<span className="text-[7px] font-bold tracking-widest" style={{ ...O, color: "#f83cc5" }}>LIVE</span>
|
|
247
196
|
</div>
|
|
248
197
|
)}
|
|
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
|
-
)}
|
|
198
|
+
{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
199
|
</div>
|
|
255
200
|
|
|
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>
|
|
201
|
+
{/* teams */}
|
|
202
|
+
<div className="flex items-center gap-1.5 justify-center py-0.5">
|
|
203
|
+
<span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color: done ? "rgba(255,255,255,0.35)" : "#fff" }}>{teamShort(match, "a")}</span>
|
|
204
|
+
<span className="text-[8px] font-medium" style={{ ...O, color: "rgba(255,255,255,0.35)" }}>vs</span>
|
|
205
|
+
<span className="barlowcondensedBold text-[18px] leading-none tracking-wide" style={{ color: done ? "rgba(255,255,255,0.35)" : "#fff" }}>{teamShort(match, "b")}</span>
|
|
265
206
|
</div>
|
|
266
207
|
|
|
267
|
-
{/*
|
|
208
|
+
{/* venue */}
|
|
268
209
|
{venue && (
|
|
269
210
|
<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>
|
|
211
|
+
<MapPin size={7} style={{ color: "rgba(255,255,255,0.35)" }} />
|
|
212
|
+
<span className="text-[7px] truncate max-w-[110px]" style={{ ...O, color: "rgba(255,255,255,0.5)" }}>{venue}</span>
|
|
274
213
|
</div>
|
|
275
214
|
)}
|
|
276
215
|
|
|
277
|
-
{/*
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
isCompleted={isCompleted}
|
|
281
|
-
onPoolPress={onPoolPress ? (pool) => onPoolPress(pool, match) : undefined}
|
|
282
|
-
partnerSource={partnerSource}
|
|
283
|
-
/>
|
|
284
|
-
</motion.div>
|
|
216
|
+
{/* pools */}
|
|
217
|
+
<MatchPools matchId={match.id} isCompleted={done} onPoolPress={onPoolPress ? p => onPoolPress(p, match) : undefined} partnerSource={partnerSource} />
|
|
218
|
+
</div>
|
|
285
219
|
</div>
|
|
286
220
|
);
|
|
287
221
|
}
|
|
288
222
|
|
|
289
|
-
/* ──
|
|
223
|
+
/* ── day column ── */
|
|
290
224
|
|
|
291
|
-
function
|
|
292
|
-
const
|
|
293
|
-
const hasLive = day.matches.some(
|
|
294
|
-
const
|
|
225
|
+
function DayCol({ day, idx, onPoolPress, ps }: { day: IMatchDay; idx: number; onPoolPress?: (p: ITDPool, m: ITDMatch) => void; ps?: string }) {
|
|
226
|
+
const isToday = day.label === "TODAY";
|
|
227
|
+
const hasLive = day.matches.some(m => m.status === MS.LIVE);
|
|
228
|
+
const allDone = day.matches.every(m => m.status === MS.COMPLETED);
|
|
295
229
|
|
|
296
230
|
return (
|
|
297
231
|
<motion.div
|
|
298
|
-
initial={{ opacity: 0, y:
|
|
232
|
+
initial={{ opacity: 0, y: 8 }}
|
|
299
233
|
animate={{ opacity: 1, y: 0 }}
|
|
300
|
-
transition={{ duration: 0.
|
|
234
|
+
transition={{ duration: 0.3, delay: idx * 0.03 }}
|
|
301
235
|
className="shrink-0 flex flex-col gap-1.5"
|
|
302
236
|
style={{ scrollSnapAlign: "start", width: "150px" }}
|
|
303
237
|
>
|
|
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
|
-
)}
|
|
238
|
+
<div className="flex flex-col items-center pb-1">
|
|
239
|
+
<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>
|
|
240
|
+
<span className="text-[11px] font-semibold" style={{ ...O, color: hasLive ? "#f83cc5" : isToday ? "#22E3E8" : allDone ? "rgba(255,255,255,0.25)" : "#fff" }}>{day.dayMonth}</span>
|
|
241
|
+
{(isToday || hasLive) && <div className="w-4 h-[1.5px] rounded-full mt-0.5" style={{ background: hasLive ? "#f83cc5" : "#22E3E8" }} />}
|
|
320
242
|
</div>
|
|
321
|
-
{day.matches.map(
|
|
322
|
-
<MatchCard key={match.id} match={match} onPoolPress={onPoolPress} partnerSource={partnerSource} />
|
|
323
|
-
))}
|
|
243
|
+
{day.matches.map(m => <MatchCard key={m.id} match={m} onPoolPress={onPoolPress} partnerSource={ps} />)}
|
|
324
244
|
</motion.div>
|
|
325
245
|
);
|
|
326
246
|
}
|
|
327
247
|
|
|
328
|
-
/* ──
|
|
248
|
+
/* ── main ── */
|
|
329
249
|
|
|
330
250
|
interface MatchCalendarProps {
|
|
331
251
|
tournamentId?: number;
|
|
@@ -334,53 +254,41 @@ interface MatchCalendarProps {
|
|
|
334
254
|
}
|
|
335
255
|
|
|
336
256
|
export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: MatchCalendarProps) {
|
|
337
|
-
const
|
|
257
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
338
258
|
const { matches, isLoading, error } = useTDMatches({ tournamentId, limit: 100 });
|
|
339
259
|
const days = useMemo(() => groupByDate(matches), [matches]);
|
|
340
260
|
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
261
|
+
const scrollIdx = useMemo(() => {
|
|
262
|
+
const ti = days.findIndex(d => today(d.date));
|
|
263
|
+
if (ti >= 0) return ti;
|
|
344
264
|
const now = new Date();
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
return 0;
|
|
265
|
+
const ui = days.findIndex(d => d.date >= now);
|
|
266
|
+
return ui >= 0 ? Math.max(0, ui) : 0;
|
|
348
267
|
}, [days]);
|
|
349
268
|
|
|
350
269
|
useEffect(() => {
|
|
351
|
-
if (
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
270
|
+
if (ref.current && scrollIdx > 0) {
|
|
271
|
+
const t = ref.current.children[scrollIdx] as HTMLElement;
|
|
272
|
+
if (t) ref.current.scrollTo({ left: t.offsetLeft - 16, behavior: "smooth" });
|
|
354
273
|
}
|
|
355
|
-
}, [
|
|
356
|
-
|
|
357
|
-
const tournament = matches[0]?.tournament;
|
|
274
|
+
}, [scrollIdx, days]);
|
|
358
275
|
|
|
359
|
-
|
|
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
|
-
}
|
|
276
|
+
const tourney = matches[0]?.tournament;
|
|
366
277
|
|
|
278
|
+
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
279
|
if (error || matches.length === 0) return null;
|
|
368
280
|
|
|
369
281
|
return (
|
|
370
282
|
<div className="relative z-30 w-full mt-5 mb-3">
|
|
371
|
-
{
|
|
283
|
+
{tourney && (
|
|
372
284
|
<div className="px-5 mb-2.5">
|
|
373
285
|
<div className="flex items-center gap-2 mb-0.5">
|
|
374
286
|
<Trophy size={13} style={{ color: "#22E3E8" }} />
|
|
375
|
-
<span className="barlowcondensedBold text-[15px] tracking-wide text-white">{
|
|
287
|
+
<span className="barlowcondensedBold text-[15px] tracking-wide text-white">{tourney.name}</span>
|
|
376
288
|
</div>
|
|
377
289
|
<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>
|
|
290
|
+
{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>}
|
|
291
|
+
<span className="text-[10px] text-white/50" style={O}>{matches.length} matches</span>
|
|
384
292
|
</div>
|
|
385
293
|
</div>
|
|
386
294
|
)}
|
|
@@ -388,17 +296,11 @@ export function MatchCalendar({ tournamentId, onPoolPress, partnerSource }: Matc
|
|
|
388
296
|
<div className="flex items-center gap-2 px-5 mb-2.5">
|
|
389
297
|
<Calendar size={12} style={{ color: "#22E3E8" }} />
|
|
390
298
|
<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.
|
|
299
|
+
<div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
392
300
|
</div>
|
|
393
301
|
|
|
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
|
-
))}
|
|
302
|
+
<div ref={ref} className="flex gap-2.5 overflow-x-auto px-5 pb-3 scrollbar-hide" style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}>
|
|
303
|
+
{days.map((d, i) => <DayCol key={d.dateKey} day={d} idx={i} onPoolPress={onPoolPress} ps={partnerSource} />)}
|
|
402
304
|
</div>
|
|
403
305
|
</div>
|
|
404
306
|
);
|