@devrongx/games 0.2.0 → 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.2.0",
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",
@@ -3,12 +3,10 @@
3
3
 
4
4
  import { useRef } from "react";
5
5
  import { motion } from "framer-motion";
6
- import { Calendar, MapPin, Loader2 } from "lucide-react";
6
+ import { Calendar, MapPin, Loader2, Zap } from "lucide-react";
7
7
  import { useTDMatches } from "./useTDMatches";
8
8
  import type { ITDMatch } from "./types";
9
9
 
10
- const OUTFIT = { fontFamily: "Outfit, sans-serif" };
11
-
12
10
  // TD match status enum values
13
11
  const MATCH_STATUS = {
14
12
  UPCOMING: 1,
@@ -16,20 +14,12 @@ const MATCH_STATUS = {
16
14
  COMPLETED: 5,
17
15
  } as const;
18
16
 
19
- function formatMatchDate(iso: string): { day: string; month: string; time: string; weekday: string } {
17
+ function formatMatchDate(iso: string): { dayMonth: string; time: string } {
20
18
  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" };
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 };
33
23
  }
34
24
 
35
25
  function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
@@ -39,81 +29,139 @@ function getTeamShortName(match: ITDMatch, team: "a" | "b"): string {
39
29
 
40
30
  interface MatchCardProps {
41
31
  match: ITDMatch;
32
+ index: number;
42
33
  }
43
34
 
44
- function MatchCard({ match }: MatchCardProps) {
45
- const status = getStatusLabel(match);
35
+ function MatchCard({ match, index }: MatchCardProps) {
46
36
  const date = match.scheduled_start_at ? formatMatchDate(match.scheduled_start_at) : null;
37
+ const isLive = match.status === MATCH_STATUS.LIVE;
47
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;
48
41
 
49
42
  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
- }}
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" }}
57
49
  >
58
50
  <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)" }}
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
+ }}
61
64
  >
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} &middot; {date.time}
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]" />
68
71
  </span>
69
72
  </div>
70
73
  )}
71
74
 
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" />
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">
87
102
  <span
88
- className="text-[9px] text-white/30 truncate max-w-[130px]"
89
- style={OUTFIT}
103
+ className="barlowcondensedBold text-[22px] leading-none"
104
+ style={{ color: isCompleted ? "rgba(255,255,255,0.35)" : "#fff" }}
90
105
  >
91
- {match.venue.city || match.venue.name}
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")}
92
119
  </span>
93
120
  </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
121
 
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"}
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}
113
161
  </div>
114
- )}
162
+ </div>
115
163
  </div>
116
- </div>
164
+ </motion.div>
117
165
  );
118
166
  }
119
167
 
@@ -125,13 +173,13 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
125
173
  const scrollRef = useRef<HTMLDivElement>(null);
126
174
  const { matches, isLoading, error } = useTDMatches({
127
175
  tournamentId,
128
- limit: 20,
176
+ limit: 50,
129
177
  });
130
178
 
131
179
  if (isLoading) {
132
180
  return (
133
- <div className="flex items-center justify-center py-6">
134
- <Loader2 size={18} className="animate-spin text-white/30" />
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)" }} />
135
183
  </div>
136
184
  );
137
185
  }
@@ -139,35 +187,27 @@ export function MatchCalendar({ tournamentId }: MatchCalendarProps) {
139
187
  if (error || matches.length === 0) return null;
140
188
 
141
189
  return (
142
- <div className="relative z-30 w-full mb-4">
190
+ <div className="relative z-30 w-full mb-2 mt-2">
143
191
  {/* 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
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
148
196
  </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
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}
152
200
  </span>
153
201
  </div>
154
202
 
155
203
  {/* Scrollable match strip */}
156
204
  <div
157
205
  ref={scrollRef}
158
- className="flex gap-3 overflow-x-auto px-5 pb-2 scrollbar-hidden"
206
+ className="flex gap-2.5 overflow-x-auto px-5 pb-2 scrollbar-hidden"
159
207
  style={{ scrollSnapType: "x mandatory", WebkitOverflowScrolling: "touch" }}
160
208
  >
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>
209
+ {matches.map((match, i) => (
210
+ <MatchCard key={match.id} match={match} index={i} />
171
211
  ))}
172
212
  </div>
173
213
  </div>