@devrongx/games 0.1.0 → 0.2.0
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/games/prematch-bets/PreMatchQuestions.tsx +17 -69
- package/src/index.ts +4 -0
- package/src/matches/MatchCalendar.tsx +175 -0
- package/src/matches/fetcher.ts +33 -0
- package/src/matches/index.ts +16 -0
- package/src/matches/store.ts +15 -0
- package/src/matches/types.ts +129 -0
- package/src/matches/useTDMatches.ts +48 -0
package/package.json
CHANGED
|
@@ -13,10 +13,8 @@ interface PreMatchQuestionsProps {
|
|
|
13
13
|
onComplete: (selections: IUserBets) => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// ──
|
|
17
|
-
|
|
18
|
-
// Within each word, character delays follow an exponential curve.
|
|
19
|
-
const useSassyTypewriter = (text: string, delay = 0) => {
|
|
16
|
+
// ── Smooth typewriter — constant speed, clean and simple ──
|
|
17
|
+
const useTypewriter = (text: string, speed = 45, delay = 0) => {
|
|
20
18
|
const [displayed, setDisplayed] = useState("");
|
|
21
19
|
const [done, setDone] = useState(false);
|
|
22
20
|
|
|
@@ -24,74 +22,28 @@ const useSassyTypewriter = (text: string, delay = 0) => {
|
|
|
24
22
|
setDisplayed("");
|
|
25
23
|
setDone(false);
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const wordStyles = ["drag", "snap", "burst", "drag", "snap", "burst"];
|
|
36
|
-
|
|
37
|
-
words.forEach((word, wIdx) => {
|
|
38
|
-
const style = wordStyles[wIdx % wordStyles.length];
|
|
39
|
-
const len = word.length;
|
|
40
|
-
|
|
41
|
-
for (let c = 0; c < len; c++) {
|
|
42
|
-
const t = len > 1 ? c / (len - 1) : 0; // 0..1 progress through word
|
|
43
|
-
|
|
44
|
-
let ms: number;
|
|
45
|
-
if (style === "burst") {
|
|
46
|
-
// Starts fast (15ms), exponentially decelerates to ~80ms
|
|
47
|
-
ms = 15 + 65 * Math.pow(t, 2.5);
|
|
48
|
-
} else if (style === "drag") {
|
|
49
|
-
// Starts slow (90ms), exponentially accelerates to ~12ms
|
|
50
|
-
ms = 90 - 78 * Math.pow(t, 2.2);
|
|
51
|
-
} else {
|
|
52
|
-
// snap — near instant, 8ms per char
|
|
53
|
-
ms = 8;
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
let i = 0;
|
|
27
|
+
const interval = setInterval(() => {
|
|
28
|
+
i++;
|
|
29
|
+
setDisplayed(text.slice(0, i));
|
|
30
|
+
if (i >= text.length) {
|
|
31
|
+
clearInterval(interval);
|
|
32
|
+
setDone(true);
|
|
54
33
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
charDelays.push(wIdx < words.length - 1 ? 120 : 0);
|
|
59
|
-
});
|
|
34
|
+
}, speed);
|
|
35
|
+
return () => clearInterval(interval);
|
|
36
|
+
}, delay);
|
|
60
37
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
let charIdx = 0;
|
|
64
|
-
|
|
65
|
-
// Schedule each character reveal
|
|
66
|
-
for (let i = 0; i < charDelays.length; i++) {
|
|
67
|
-
cumulative += charDelays[i];
|
|
68
|
-
const idx = charIdx;
|
|
69
|
-
// The space between words is at charDelays entries where we push 120ms pause
|
|
70
|
-
// but we need to track actual characters in text
|
|
71
|
-
if (i < charDelays.length && idx <= text.length) {
|
|
72
|
-
timers.push(setTimeout(() => {
|
|
73
|
-
setDisplayed(text.slice(0, idx + 1));
|
|
74
|
-
if (idx + 1 >= text.length) setDone(true);
|
|
75
|
-
}, cumulative));
|
|
76
|
-
}
|
|
77
|
-
// Only increment charIdx for actual characters (not the inter-word pauses)
|
|
78
|
-
// Words joined by spaces: after each word's chars, next char is space
|
|
79
|
-
charIdx++;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return () => timers.forEach(clearTimeout);
|
|
83
|
-
}, [text, delay]);
|
|
38
|
+
return () => clearTimeout(timer);
|
|
39
|
+
}, [text, speed, delay]);
|
|
84
40
|
|
|
85
41
|
return { displayed, done };
|
|
86
42
|
};
|
|
87
43
|
|
|
88
44
|
// ── Start slide ──
|
|
89
45
|
const StartSlide = ({ onStart }: { onStart: () => void }) => {
|
|
90
|
-
const line1 =
|
|
91
|
-
const line2 = useSassyTypewriter(
|
|
92
|
-
"Pick your predictions first. We'll set up bet amounts after.",
|
|
93
|
-
line1.done ? 0 : 1800,
|
|
94
|
-
);
|
|
46
|
+
const line1 = useTypewriter("Choose your answers", 45, 300);
|
|
95
47
|
|
|
96
48
|
return (
|
|
97
49
|
<div className="flex flex-col items-center justify-center h-full px-8 gap-8">
|
|
@@ -100,14 +52,10 @@ const StartSlide = ({ onStart }: { onStart: () => void }) => {
|
|
|
100
52
|
{line1.displayed}
|
|
101
53
|
{!line1.done && <span className="inline-block w-[2px] h-[18px] bg-[#22E3E8] ml-[2px] align-middle animate-pulse" />}
|
|
102
54
|
</p>
|
|
103
|
-
<p className="text-[14px] text-white/50 font-medium text-center leading-relaxed max-w-[260px] min-h-[44px]" style={OUTFIT}>
|
|
104
|
-
{line2.displayed}
|
|
105
|
-
{line1.done && !line2.done && <span className="inline-block w-[2px] h-[14px] bg-white/30 ml-[2px] align-middle animate-pulse" />}
|
|
106
|
-
</p>
|
|
107
55
|
</div>
|
|
108
56
|
|
|
109
57
|
{/* Let's Go — simple cyan text link with arrow */}
|
|
110
|
-
{
|
|
58
|
+
{line1.done && (
|
|
111
59
|
<motion.button
|
|
112
60
|
initial={{ opacity: 0 }}
|
|
113
61
|
animate={{ opacity: 1 }}
|
package/src/index.ts
CHANGED
|
@@ -8,3 +8,7 @@ export type { IGamePopupState } from "./core/types";
|
|
|
8
8
|
export { PreMatchBetsPopup } from "./games/prematch-bets/PreMatchBetsPopup";
|
|
9
9
|
export { CSK_VS_RCB_CHALLENGE, CSK_VS_RCB_POOL, calcBetSummary, buildPoolLeaderboard, generatePool, calcPrizePool, calcRankPayout, calcParlayMultiplier, calcDisplayReward, optionReward, deriveParlayGroups, deriveMarketToParlay, extractUserBets } from "./games/prematch-bets/config";
|
|
10
10
|
export type { IChallengeConfig, IChallengeMarket, IChallengeOption, IChallengeTeam, IChallengeGame, IChallengeSection, IRankBracket, IParlayConfig, IUserBets, IBetEntry, IBetSummary, ILeaderboardEntry, IMiniLeaderboard, IPoolData } from "./games/prematch-bets/config";
|
|
11
|
+
|
|
12
|
+
// Matches — TD match calendar, data fetching, and types
|
|
13
|
+
export { configureTDClient, fetchTDMatches, useTDMatches, MatchCalendar } from "./matches";
|
|
14
|
+
export type { ITDClientConfig, ITDMatch, ITDTeam, ITDPlayer, ITDVenue, ITDTournament, ITDMatchesResponse, ITDMatchesParams, UseTDMatchesResult } from "./matches";
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// @devrongx/games — matches/MatchCalendar.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useRef } from "react";
|
|
5
|
+
import { motion } from "framer-motion";
|
|
6
|
+
import { Calendar, MapPin, Loader2 } from "lucide-react";
|
|
7
|
+
import { useTDMatches } from "./useTDMatches";
|
|
8
|
+
import type { ITDMatch } from "./types";
|
|
9
|
+
|
|
10
|
+
const OUTFIT = { fontFamily: "Outfit, sans-serif" };
|
|
11
|
+
|
|
12
|
+
// TD match status enum values
|
|
13
|
+
const MATCH_STATUS = {
|
|
14
|
+
UPCOMING: 1,
|
|
15
|
+
LIVE: 2,
|
|
16
|
+
COMPLETED: 5,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
function formatMatchDate(iso: string): { day: string; month: string; time: string; weekday: string } {
|
|
20
|
+
const d = new Date(iso);
|
|
21
|
+
return {
|
|
22
|
+
day: d.getDate().toString(),
|
|
23
|
+
month: d.toLocaleString("en-US", { month: "short" }).toUpperCase(),
|
|
24
|
+
time: d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }),
|
|
25
|
+
weekday: d.toLocaleString("en-US", { weekday: "short" }).toUpperCase(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getStatusLabel(match: ITDMatch): { text: string; color: string } {
|
|
30
|
+
if (match.status === MATCH_STATUS.LIVE) return { text: "LIVE", color: "#f83cc5" };
|
|
31
|
+
if (match.status === MATCH_STATUS.COMPLETED) return { text: "COMPLETED", color: "rgba(255,255,255,0.35)" };
|
|
32
|
+
return { text: "UPCOMING", color: "#22E3E8" };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
|
|
36
|
+
const t = team === "a" ? match.team_a : match.team_b;
|
|
37
|
+
return t.short_name || t.name.slice(0, 3).toUpperCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface MatchCardProps {
|
|
41
|
+
match: ITDMatch;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function MatchCard({ match }: MatchCardProps) {
|
|
45
|
+
const status = getStatusLabel(match);
|
|
46
|
+
const date = match.scheduled_start_at ? formatMatchDate(match.scheduled_start_at) : null;
|
|
47
|
+
const isCompleted = match.status === MATCH_STATUS.COMPLETED;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className="shrink-0 w-[160px] rounded-xl p-[1px] overflow-hidden"
|
|
52
|
+
style={{
|
|
53
|
+
background: isCompleted
|
|
54
|
+
? "rgba(255,255,255,0.08)"
|
|
55
|
+
: "linear-gradient(135deg, rgba(34,227,232,0.3), rgba(153,69,255,0.3))",
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
className="flex flex-col items-center rounded-xl px-3 py-3 h-full"
|
|
60
|
+
style={{ background: isCompleted ? "rgba(10,10,18,0.85)" : "rgba(10,10,18,0.95)" }}
|
|
61
|
+
>
|
|
62
|
+
{/* Date */}
|
|
63
|
+
{date && (
|
|
64
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
65
|
+
<Calendar size={10} className="text-white/40" />
|
|
66
|
+
<span className="text-[10px] text-white/50 tracking-wide" style={OUTFIT}>
|
|
67
|
+
{date.weekday} {date.day} {date.month} · {date.time}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{/* Teams */}
|
|
73
|
+
<div className="flex items-center gap-2 w-full justify-center">
|
|
74
|
+
<span className="barlowcondensedBold text-[20px] text-white leading-none">
|
|
75
|
+
{getTeamShortName(match, "a")}
|
|
76
|
+
</span>
|
|
77
|
+
<span className="text-[10px] text-white/30 barlowCondensedSemiBold">VS</span>
|
|
78
|
+
<span className="barlowcondensedBold text-[20px] text-white leading-none">
|
|
79
|
+
{getTeamShortName(match, "b")}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Venue */}
|
|
84
|
+
{match.venue && (
|
|
85
|
+
<div className="flex items-center gap-1 mt-1.5">
|
|
86
|
+
<MapPin size={8} className="text-white/30 shrink-0" />
|
|
87
|
+
<span
|
|
88
|
+
className="text-[9px] text-white/30 truncate max-w-[130px]"
|
|
89
|
+
style={OUTFIT}
|
|
90
|
+
>
|
|
91
|
+
{match.venue.city || match.venue.name}
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Status badge */}
|
|
97
|
+
<div
|
|
98
|
+
className="mt-2 px-2 py-0.5 rounded-full"
|
|
99
|
+
style={{ background: `${status.color}15` }}
|
|
100
|
+
>
|
|
101
|
+
<span
|
|
102
|
+
className="text-[9px] barlowcondensedBold tracking-wider"
|
|
103
|
+
style={{ color: status.color }}
|
|
104
|
+
>
|
|
105
|
+
{status.text}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Score for completed matches */}
|
|
110
|
+
{isCompleted && match.innings_summaries_json && Array.isArray(match.innings_summaries_json) && (
|
|
111
|
+
<div className="mt-1.5 text-[9px] text-white/40" style={OUTFIT}>
|
|
112
|
+
{match.winner ? `${match.winner.short_name || match.winner.name} won` : "No result"}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface MatchCalendarProps {
|
|
121
|
+
tournamentId?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
|
|
125
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
126
|
+
const { matches, isLoading, error } = useTDMatches({
|
|
127
|
+
tournamentId,
|
|
128
|
+
limit: 20,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (isLoading) {
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex items-center justify-center py-6">
|
|
134
|
+
<Loader2 size={18} className="animate-spin text-white/30" />
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (error || matches.length === 0) return null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="relative z-30 w-full mb-4">
|
|
143
|
+
{/* Section header */}
|
|
144
|
+
<div className="flex items-center gap-2 px-5 mb-3">
|
|
145
|
+
<Calendar size={14} className="text-[#22E3E8]" />
|
|
146
|
+
<span className="barlowCondensedSemiBold text-[15px] text-white/70 tracking-wide">
|
|
147
|
+
MATCH CALENDAR
|
|
148
|
+
</span>
|
|
149
|
+
<div className="flex-1 h-px bg-white/5" />
|
|
150
|
+
<span className="text-[11px] text-white/30" style={OUTFIT}>
|
|
151
|
+
{matches.length} matches
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Scrollable match strip */}
|
|
156
|
+
<div
|
|
157
|
+
ref={scrollRef}
|
|
158
|
+
className="flex gap-3 overflow-x-auto px-5 pb-2 scrollbar-hidden"
|
|
159
|
+
style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
|
|
160
|
+
>
|
|
161
|
+
{matches.map((match) => (
|
|
162
|
+
<motion.div
|
|
163
|
+
key={match.id}
|
|
164
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
165
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
166
|
+
transition={{ duration: 0.3 }}
|
|
167
|
+
style={{ scrollSnapAlign: "start" }}
|
|
168
|
+
>
|
|
169
|
+
<MatchCard match={match} />
|
|
170
|
+
</motion.div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @devrongx/games — matches/fetcher.ts
|
|
2
|
+
// Fetch wrapper using native fetch (no axios dep needed in the package).
|
|
3
|
+
// Reads config from the Zustand store set by configureTDClient().
|
|
4
|
+
|
|
5
|
+
import { useTDClientStore } from "./store";
|
|
6
|
+
import type { ITDMatchesParams, ITDMatchesResponse } from "./types";
|
|
7
|
+
|
|
8
|
+
export async function fetchTDMatches(
|
|
9
|
+
params?: ITDMatchesParams,
|
|
10
|
+
): Promise<ITDMatchesResponse> {
|
|
11
|
+
const { baseURL, getToken } = useTDClientStore.getState();
|
|
12
|
+
|
|
13
|
+
const url = new URL(`${baseURL}/api/v2/matches`);
|
|
14
|
+
if (params) {
|
|
15
|
+
for (const [key, value] of Object.entries(params)) {
|
|
16
|
+
if (value !== undefined && value !== null) {
|
|
17
|
+
url.searchParams.set(key, String(value));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const headers: HeadersInit = { Accept: "application/json" };
|
|
23
|
+
const token = getToken();
|
|
24
|
+
if (token) {
|
|
25
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const res = await fetch(url.toString(), { headers });
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(`TD API error: ${res.status} ${res.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @devrongx/games — matches barrel export
|
|
2
|
+
export { configureTDClient } from "./store";
|
|
3
|
+
export { fetchTDMatches } from "./fetcher";
|
|
4
|
+
export { useTDMatches } from "./useTDMatches";
|
|
5
|
+
export type { UseTDMatchesResult } from "./useTDMatches";
|
|
6
|
+
export { MatchCalendar } from "./MatchCalendar";
|
|
7
|
+
export type {
|
|
8
|
+
ITDClientConfig,
|
|
9
|
+
ITDMatch,
|
|
10
|
+
ITDTeam,
|
|
11
|
+
ITDPlayer,
|
|
12
|
+
ITDVenue,
|
|
13
|
+
ITDTournament,
|
|
14
|
+
ITDMatchesResponse,
|
|
15
|
+
ITDMatchesParams,
|
|
16
|
+
} from "./types";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @devrongx/games — matches/store.ts
|
|
2
|
+
// Zustand store for TD API client configuration.
|
|
3
|
+
// Consuming apps call configureTDClient() once at startup.
|
|
4
|
+
|
|
5
|
+
import { create, type StoreApi, type UseBoundStore } from "zustand";
|
|
6
|
+
import type { ITDClientConfig } from "./types";
|
|
7
|
+
|
|
8
|
+
export const useTDClientStore: UseBoundStore<StoreApi<ITDClientConfig>> = create<ITDClientConfig>(() => ({
|
|
9
|
+
baseURL: "",
|
|
10
|
+
getToken: () => null,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
export function configureTDClient(config: ITDClientConfig): void {
|
|
14
|
+
useTDClientStore.setState(config);
|
|
15
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @devrongx/games — matches/types.ts
|
|
2
|
+
// Types matching the Prisma response from TD's GET /api/v2/matches exactly.
|
|
3
|
+
// listMatches includes: { team_a: true, team_b: true, winner: true, venue: true,
|
|
4
|
+
// man_of_the_match: true, tournament: { select: { id, name } } }
|
|
5
|
+
|
|
6
|
+
// ── Prisma `teams` model (full row, returned by `include: { team_a: true }`) ──
|
|
7
|
+
export interface ITDTeam {
|
|
8
|
+
id: number;
|
|
9
|
+
graph_id: string | null;
|
|
10
|
+
name: string;
|
|
11
|
+
short_name: string | null;
|
|
12
|
+
sport: number;
|
|
13
|
+
country_code: number | null;
|
|
14
|
+
founded_year: number | null;
|
|
15
|
+
attributes_json: Record<string, unknown>;
|
|
16
|
+
images_json: unknown[];
|
|
17
|
+
colors_json: unknown[];
|
|
18
|
+
status: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Prisma `players` model (full row, returned by `include: { man_of_the_match: true }`) ──
|
|
24
|
+
export interface ITDPlayer {
|
|
25
|
+
id: number;
|
|
26
|
+
graph_id: string | null;
|
|
27
|
+
full_name: string;
|
|
28
|
+
sport: number;
|
|
29
|
+
country_code: number | null;
|
|
30
|
+
date_of_birth: string | null;
|
|
31
|
+
batting_style: number | null;
|
|
32
|
+
bowling_style: number | null;
|
|
33
|
+
role: number | null;
|
|
34
|
+
images_json: Record<string, unknown> | null;
|
|
35
|
+
status: number;
|
|
36
|
+
created_at: string;
|
|
37
|
+
updated_at: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Prisma `venues` model (full row, returned by `include: { venue: true }`) ──
|
|
41
|
+
export interface ITDVenue {
|
|
42
|
+
id: number;
|
|
43
|
+
name: string;
|
|
44
|
+
sport: number;
|
|
45
|
+
address: string | null;
|
|
46
|
+
city: string | null;
|
|
47
|
+
country: number | null;
|
|
48
|
+
latitude: string | null;
|
|
49
|
+
longitude: string | null;
|
|
50
|
+
images_json: Record<string, unknown>;
|
|
51
|
+
created_at: string;
|
|
52
|
+
updated_at: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Prisma `tournaments` model (partial, returned by `select: { id, name }`) ──
|
|
56
|
+
export interface ITDTournament {
|
|
57
|
+
id: number;
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Prisma `matches` model with included relations ──
|
|
62
|
+
export interface ITDMatch {
|
|
63
|
+
id: number;
|
|
64
|
+
graph_id: string | null;
|
|
65
|
+
tournament_id: number | null;
|
|
66
|
+
stage_id: number | null;
|
|
67
|
+
group_id: number | null;
|
|
68
|
+
venue_id: number | null;
|
|
69
|
+
scheduled_start_at: string | null;
|
|
70
|
+
actual_start_at: string | null;
|
|
71
|
+
actual_end_at: string | null;
|
|
72
|
+
status: number;
|
|
73
|
+
result_status: number | null;
|
|
74
|
+
result_margin_type: number | null;
|
|
75
|
+
result_margin_value: number | null;
|
|
76
|
+
format: number | null;
|
|
77
|
+
overs_per_innings: number | null;
|
|
78
|
+
team_a_id: number;
|
|
79
|
+
team_b_id: number;
|
|
80
|
+
toss_winner_team_id: number | null;
|
|
81
|
+
toss_decision: number | null;
|
|
82
|
+
winner_team_id: number | null;
|
|
83
|
+
man_of_the_match_player_id: number | null;
|
|
84
|
+
rating_popularity: number;
|
|
85
|
+
rating_interesting: number;
|
|
86
|
+
rating_evergreen: number;
|
|
87
|
+
match_number: number | null;
|
|
88
|
+
bracket_round_no: number | null;
|
|
89
|
+
bracket_match_no: number | null;
|
|
90
|
+
winner_of_a_match_id: number | null;
|
|
91
|
+
winner_of_b_match_id: number | null;
|
|
92
|
+
loser_of_a_match_id: number | null;
|
|
93
|
+
loser_of_b_match_id: number | null;
|
|
94
|
+
innings_summaries_json: unknown[] | null;
|
|
95
|
+
og_assets: Record<string, unknown> | null;
|
|
96
|
+
og_assets_expires_at: string | null;
|
|
97
|
+
created_at: string;
|
|
98
|
+
updated_at: string;
|
|
99
|
+
// Included relations
|
|
100
|
+
team_a: ITDTeam;
|
|
101
|
+
team_b: ITDTeam;
|
|
102
|
+
winner: ITDTeam | null;
|
|
103
|
+
venue: ITDVenue | null;
|
|
104
|
+
man_of_the_match: ITDPlayer | null;
|
|
105
|
+
tournament: ITDTournament | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── API response shape ──
|
|
109
|
+
export interface ITDMatchesResponse {
|
|
110
|
+
success: boolean;
|
|
111
|
+
data: ITDMatch[];
|
|
112
|
+
total: number;
|
|
113
|
+
user_id: number | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ITDMatchesParams {
|
|
117
|
+
tournamentId?: number;
|
|
118
|
+
status?: number;
|
|
119
|
+
from?: string;
|
|
120
|
+
to?: string;
|
|
121
|
+
limit?: number;
|
|
122
|
+
offset?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Client configuration (set once by consuming app) ──
|
|
126
|
+
export interface ITDClientConfig {
|
|
127
|
+
baseURL: string;
|
|
128
|
+
getToken: () => string | null;
|
|
129
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @devrongx/games — matches/useTDMatches.ts
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useCallback } from "react";
|
|
5
|
+
import { fetchTDMatches } from "./fetcher";
|
|
6
|
+
import type { ITDMatch, ITDMatchesParams, ITDMatchesResponse } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface UseTDMatchesResult {
|
|
9
|
+
matches: ITDMatch[];
|
|
10
|
+
total: number;
|
|
11
|
+
userId: number | null;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
refetch: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useTDMatches(params?: ITDMatchesParams): UseTDMatchesResult {
|
|
18
|
+
const [matches, setMatches] = useState<ITDMatch[]>([]);
|
|
19
|
+
const [total, setTotal] = useState(0);
|
|
20
|
+
const [userId, setUserId] = useState<number | null>(null);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const paramsKey = JSON.stringify(params);
|
|
25
|
+
|
|
26
|
+
const fetchData = useCallback(async () => {
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
const res: ITDMatchesResponse = await fetchTDMatches(params);
|
|
31
|
+
setMatches(res.data);
|
|
32
|
+
setTotal(res.total);
|
|
33
|
+
setUserId(res.user_id);
|
|
34
|
+
} catch (err: unknown) {
|
|
35
|
+
const message = err instanceof Error ? err.message : "Failed to fetch matches";
|
|
36
|
+
setError(message);
|
|
37
|
+
} finally {
|
|
38
|
+
setIsLoading(false);
|
|
39
|
+
}
|
|
40
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
+
}, [paramsKey]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
fetchData();
|
|
45
|
+
}, [fetchData]);
|
|
46
|
+
|
|
47
|
+
return { matches, total, userId, isLoading, error, refetch: fetchData };
|
|
48
|
+
}
|