@devrongx/games 0.1.1 → 0.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrongx/games",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Game UI components for sports prediction markets",
5
5
  "license": "MIT",
6
6
  "main": "./src/index.ts",
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,215 @@
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, Zap } from "lucide-react";
7
+ import { useTDMatches } from "./useTDMatches";
8
+ import type { ITDMatch } from "./types";
9
+
10
+ // TD match status enum values
11
+ const MATCH_STATUS = {
12
+ UPCOMING: 1,
13
+ LIVE: 2,
14
+ COMPLETED: 5,
15
+ } as const;
16
+
17
+ function formatMatchDate(iso: string): { dayMonth: string; time: string } {
18
+ const d = new Date(iso);
19
+ const day = d.getDate();
20
+ const month = d.toLocaleString("en-US", { month: "short" }).toUpperCase();
21
+ const time = d.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true });
22
+ return { dayMonth: `${day} ${month}`, time };
23
+ }
24
+
25
+ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
26
+ const t = team === "a" ? match.team_a : match.team_b;
27
+ return t.short_name || t.name.slice(0, 3).toUpperCase();
28
+ }
29
+
30
+ interface MatchCardProps {
31
+ match: ITDMatch;
32
+ index: number;
33
+ }
34
+
35
+ function MatchCard({ match, index }: MatchCardProps) {
36
+ const date = match.scheduled_start_at ? formatMatchDate(match.scheduled_start_at) : null;
37
+ const isLive = match.status === MATCH_STATUS.LIVE;
38
+ const isCompleted = match.status === MATCH_STATUS.COMPLETED;
39
+ const isUpcoming = match.status === MATCH_STATUS.UPCOMING;
40
+ const venue = match.venue?.city || match.venue?.name;
41
+
42
+ return (
43
+ <motion.div
44
+ initial={{ opacity: 0, y: 12 }}
45
+ animate={{ opacity: 1, y: 0 }}
46
+ transition={{ duration: 0.35, delay: index * 0.04 }}
47
+ className="shrink-0"
48
+ style={{ scrollSnapAlign: "start" }}
49
+ >
50
+ <div
51
+ className="relative w-[140px] rounded-2xl overflow-hidden"
52
+ style={{
53
+ background: isLive
54
+ ? "linear-gradient(160deg, rgba(248,60,197,0.15), rgba(248,60,197,0.03))"
55
+ : isCompleted
56
+ ? "rgba(255,255,255,0.03)"
57
+ : "linear-gradient(160deg, rgba(34,227,232,0.08), rgba(153,69,255,0.04))",
58
+ border: isLive
59
+ ? "1px solid rgba(248,60,197,0.3)"
60
+ : isCompleted
61
+ ? "1px solid rgba(255,255,255,0.06)"
62
+ : "1px solid rgba(34,227,232,0.12)",
63
+ }}
64
+ >
65
+ {/* Live pulse indicator */}
66
+ {isLive && (
67
+ <div className="absolute top-2 right-2 flex items-center gap-1">
68
+ <span className="relative flex h-2 w-2">
69
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
70
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-[#f83cc5]" />
71
+ </span>
72
+ </div>
73
+ )}
74
+
75
+ <div className="flex flex-col items-center px-3 pt-3 pb-2.5">
76
+ {/* Date */}
77
+ {date && (
78
+ <div className="flex flex-col items-center mb-2.5">
79
+ <span
80
+ className="text-[11px] font-semibold tracking-wide"
81
+ style={{
82
+ fontFamily: "Outfit, sans-serif",
83
+ color: isLive ? "#f83cc5" : isCompleted ? "rgba(255,255,255,0.25)" : "rgba(34,227,232,0.7)",
84
+ }}
85
+ >
86
+ {date.dayMonth}
87
+ </span>
88
+ <span
89
+ className="text-[10px] tracking-wide"
90
+ style={{
91
+ fontFamily: "Outfit, sans-serif",
92
+ color: isCompleted ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.35)",
93
+ }}
94
+ >
95
+ {date.time}
96
+ </span>
97
+ </div>
98
+ )}
99
+
100
+ {/* Teams */}
101
+ <div className="flex items-center gap-1.5 w-full justify-center">
102
+ <span
103
+ className="barlowcondensedBold text-[22px] leading-none"
104
+ style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
105
+ >
106
+ {getTeamShortName(match, "a")}
107
+ </span>
108
+ <span
109
+ className="text-[9px] barlowCondensedSemiBold"
110
+ style={{ color: isCompleted ? "rgba(255,255,255,0.15)" : "rgba(255,255,255,0.25)" }}
111
+ >
112
+ v
113
+ </span>
114
+ <span
115
+ className="barlowcondensedBold text-[22px] leading-none"
116
+ style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
117
+ >
118
+ {getTeamShortName(match, "b")}
119
+ </span>
120
+ </div>
121
+
122
+ {/* Venue */}
123
+ {venue && (
124
+ <div className="flex items-center gap-1 mt-1.5">
125
+ <MapPin size={8} className="shrink-0" style={{ color: "rgba(255,255,255,0.2)" }} />
126
+ <span
127
+ className="text-[9px] truncate max-w-[110px]"
128
+ style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
129
+ >
130
+ {venue}
131
+ </span>
132
+ </div>
133
+ )}
134
+
135
+ {/* Status */}
136
+ <div className="mt-2">
137
+ {isLive ? (
138
+ <div className="flex items-center gap-1 px-2 py-0.5 rounded-full" style={{ background: "rgba(248,60,197,0.15)" }}>
139
+ <Zap size={8} style={{ color: "#f83cc5" }} />
140
+ <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "#f83cc5" }}>
141
+ LIVE
142
+ </span>
143
+ </div>
144
+ ) : isCompleted ? (
145
+ <span
146
+ className="text-[9px] tracking-wide"
147
+ style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}
148
+ >
149
+ {match.winner ? `${match.winner.short_name || match.winner.name} won` : "Completed"}
150
+ </span>
151
+ ) : isUpcoming ? (
152
+ <div
153
+ className="px-2 py-0.5 rounded-full"
154
+ style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.15)" }}
155
+ >
156
+ <span className="text-[8px] barlowcondensedBold tracking-widest" style={{ color: "rgba(34,227,232,0.6)" }}>
157
+ UPCOMING
158
+ </span>
159
+ </div>
160
+ ) : null}
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </motion.div>
165
+ );
166
+ }
167
+
168
+ interface MatchCalendarProps {
169
+ tournamentId?: number;
170
+ }
171
+
172
+ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
173
+ const scrollRef = useRef<HTMLDivElement>(null);
174
+ const { matches, isLoading, error } = useTDMatches({
175
+ tournamentId,
176
+ limit: 50,
177
+ });
178
+
179
+ if (isLoading) {
180
+ return (
181
+ <div className="flex items-center justify-center py-4">
182
+ <Loader2 size={16} className="animate-spin" style={{ color: "rgba(255,255,255,0.2)" }} />
183
+ </div>
184
+ );
185
+ }
186
+
187
+ if (error || matches.length === 0) return null;
188
+
189
+ return (
190
+ <div className="relative z-30 w-full mb-2 mt-2">
191
+ {/* Section header */}
192
+ <div className="flex items-center gap-2 px-5 mb-2.5">
193
+ <Calendar size={12} style={{ color: "rgba(34,227,232,0.6)" }} />
194
+ <span className="barlowCondensedSemiBold text-[13px] tracking-wider" style={{ color: "rgba(255,255,255,0.45)" }}>
195
+ MATCHES
196
+ </span>
197
+ <div className="flex-1 h-px" style={{ background: "rgba(255,255,255,0.04)" }} />
198
+ <span className="text-[10px]" style={{ fontFamily: "Outfit, sans-serif", color: "rgba(255,255,255,0.2)" }}>
199
+ {matches.length}
200
+ </span>
201
+ </div>
202
+
203
+ {/* Scrollable match strip */}
204
+ <div
205
+ ref={scrollRef}
206
+ className="flex gap-2.5 overflow-x-auto px-5 pb-2 scrollbar-hidden"
207
+ style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
208
+ >
209
+ {matches.map((match, i) => (
210
+ <MatchCard key={match.id} match={match} index={i} />
211
+ ))}
212
+ </div>
213
+ </div>
214
+ );
215
+ }
@@ -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
+ }