@vatvaghool/create-ipl-dashboard 0.1.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/README.md +75 -0
- package/package.json +27 -0
- package/src/generate-template.mjs +73 -0
- package/src/index.mjs +98 -0
- package/src/prompts.mjs +78 -0
- package/src/scaffold.mjs +129 -0
- package/src/scraper.mjs +79 -0
- package/template/.dockerignore +13 -0
- package/template/AGENTS.md +5 -0
- package/template/Dockerfile.sync +14 -0
- package/template/README.md +160 -0
- package/template/app/api/ipl/data.ts +24 -0
- package/template/app/api/ipl/route.ts +505 -0
- package/template/app/api/ipl/transfers/route.ts +261 -0
- package/template/app/api/ipl/transfers/transform.ts +156 -0
- package/template/app/api/ipl/transform.ts +20 -0
- package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
- package/template/app/api/ops/status/route.ts +225 -0
- package/template/app/components/AIRoasting.tsx +278 -0
- package/template/app/components/ColorWave.tsx +193 -0
- package/template/app/components/CrownBattle.tsx +207 -0
- package/template/app/components/DashboardContent.tsx +377 -0
- package/template/app/components/FantasyStockTicker.tsx +192 -0
- package/template/app/components/FireworksBurst.tsx +225 -0
- package/template/app/components/LiveMatchTicker.tsx +117 -0
- package/template/app/components/MatchRecapScroll.tsx +135 -0
- package/template/app/components/MatchStoryScrubber.tsx +274 -0
- package/template/app/components/PerformanceTracker.tsx +132 -0
- package/template/app/components/ProgressGlowRings.tsx +157 -0
- package/template/app/components/TeamDNAScanner.tsx +238 -0
- package/template/app/components/ThemeToggle.tsx +74 -0
- package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
- package/template/app/components/dashboard/ChartBoard.tsx +162 -0
- package/template/app/components/dashboard/LatestBadge.tsx +23 -0
- package/template/app/components/dashboard/LedgerTable.tsx +385 -0
- package/template/app/components/dashboard/SectionCard.tsx +59 -0
- package/template/app/components/dashboard/StickyMini.tsx +20 -0
- package/template/app/components/dashboard/index.ts +6 -0
- package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
- package/template/app/components/ui/DoodleSpinner.tsx +15 -0
- package/template/app/components/ui/TeamPills.tsx +41 -0
- package/template/app/data/match-points.ts +3 -0
- package/template/app/data/teams.ts +32 -0
- package/template/app/globals.css +1267 -0
- package/template/app/hooks/dashboard/index.ts +1 -0
- package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
- package/template/app/hooks/dashboardCache.ts +53 -0
- package/template/app/hooks/dashboardPolling.ts +53 -0
- package/template/app/hooks/snapshotCache.ts +47 -0
- package/template/app/hooks/useDashboardData.ts +28 -0
- package/template/app/layout.tsx +75 -0
- package/template/app/lib/aiAgent.ts +444 -0
- package/template/app/lib/config.ts +29 -0
- package/template/app/lib/dashboard/index.ts +1 -0
- package/template/app/lib/dashboard/model.ts +257 -0
- package/template/app/lib/dashboardData.ts +50 -0
- package/template/app/lib/dashboardView.ts +22 -0
- package/template/app/lib/detailedData.ts +112 -0
- package/template/app/lib/matchStatus.ts +28 -0
- package/template/app/lib/matches.ts +131 -0
- package/template/app/lib/teamBadges.ts +223 -0
- package/template/app/lib/upcomingMatches.ts +154 -0
- package/template/app/lib/useDb.ts +29 -0
- package/template/app/lib/utils/diff.ts +24 -0
- package/template/app/lib/utils/getChartColor.ts +17 -0
- package/template/app/lib/utils/getStdDeviation.ts +6 -0
- package/template/app/lib/utils/time.ts +40 -0
- package/template/app/lib/utils.ts +70 -0
- package/template/app/page.tsx +15 -0
- package/template/app/store/dashboardStore.ts +85 -0
- package/template/app/types/dashboard.ts +75 -0
- package/template/app/types.ts +130 -0
- package/template/app/utils/dashboard/index.ts +72 -0
- package/template/eslint.config.mjs +18 -0
- package/template/infra/cloud-run/README.md +68 -0
- package/template/infra/cloud-run/sync-job.yaml +32 -0
- package/template/infra/cutover/README.md +84 -0
- package/template/infra/vercel/README.md +57 -0
- package/template/next.config.ts +7 -0
- package/template/package-lock.json +7330 -0
- package/template/package.json +47 -0
- package/template/packages/ipl-dashboard-utils/README.md +316 -0
- package/template/packages/ipl-dashboard-utils/package.json +34 -0
- package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
- package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
- package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
- package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
- package/template/postcss.config.mjs +7 -0
- package/template/scripts/capture-ipl-auth.mjs +54 -0
- package/template/scripts/deploy-cloud-run-sync.sh +48 -0
- package/template/scripts/deploy-cloud-scheduler.sh +42 -0
- package/template/scripts/dev-simple.js +31 -0
- package/template/scripts/dev-welcome.mjs +38 -0
- package/template/scripts/monitor-ops-status.sh +50 -0
- package/template/scripts/seed-mongodb.ts +115 -0
- package/template/scripts/sync-cloud.mjs +50 -0
- package/template/scripts/sync-ipl.mjs +238 -0
- package/template/scripts/sync-transfers-daily.mjs +175 -0
- package/template/scripts/verify-production.mjs +108 -0
- package/template/tests/coverage-gaps.test.ts +290 -0
- package/template/tests/dashboard-polling.test.ts +96 -0
- package/template/tests/detailed-data.test.ts +60 -0
- package/template/tests/ipl-transform.test.ts +590 -0
- package/template/tests/transfers-route.test.ts +109 -0
- package/template/tests/upcoming-matches.test.ts +34 -0
- package/template/tests/utils-and-cache.test.ts +267 -0
- package/template/tsconfig.json +35 -0
- package/template/vercel.json +7 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DailyChartRow,
|
|
3
|
+
DashboardData,
|
|
4
|
+
OverallChartItem,
|
|
5
|
+
RawApiUser,
|
|
6
|
+
RawUsersSyncResult,
|
|
7
|
+
ScrapedDashboardPayload,
|
|
8
|
+
ScrapedLeaderboardItem,
|
|
9
|
+
ScrapedSquadPlayer,
|
|
10
|
+
TransformOptions,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_TOTAL_TRANSFERS = 160;
|
|
14
|
+
const FLOAT_TOLERANCE = 0.01;
|
|
15
|
+
|
|
16
|
+
const RAW_TEAM_ALIASES: Record<string, string> = {
|
|
17
|
+
"Pankaj Konde": "PKs11",
|
|
18
|
+
"Rishikesh Shinde": "Watapi",
|
|
19
|
+
"Rushabh Shah": "RushS01",
|
|
20
|
+
"Vijay Swami": "VATVAGHOOL XI",
|
|
21
|
+
"Aditya Raut": "SquadSeven9",
|
|
22
|
+
"Nilesh Birajdar": "Nilesh Birajdar",
|
|
23
|
+
"Ravi Kiran Guthula": "RKs Stallions",
|
|
24
|
+
"Rahul Sharma": "RSAwesome 11",
|
|
25
|
+
"Raviteja Jakkani": "Bat Bowl XI",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const resolveTeamName = (
|
|
29
|
+
name: string,
|
|
30
|
+
aliases: Record<string, string> | undefined,
|
|
31
|
+
) => aliases?.[name] ?? RAW_TEAM_ALIASES[name] ?? name;
|
|
32
|
+
|
|
33
|
+
const normalizeTeamName = (name: string) =>
|
|
34
|
+
name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
35
|
+
|
|
36
|
+
const calculateTotalPoints = (user: RawApiUser) =>
|
|
37
|
+
user.matches.reduce((sum, match) => sum + match.points, 0);
|
|
38
|
+
|
|
39
|
+
const roundPoint = (value: number) => Math.round(value * 10) / 10;
|
|
40
|
+
|
|
41
|
+
const compareLeadersByPoints = (
|
|
42
|
+
a: Pick<ScrapedLeaderboardItem, "name" | "points" | "rank">,
|
|
43
|
+
b: Pick<ScrapedLeaderboardItem, "name" | "points" | "rank">,
|
|
44
|
+
) => {
|
|
45
|
+
const pointDifference = b.points - a.points;
|
|
46
|
+
if (pointDifference !== 0) {
|
|
47
|
+
return pointDifference;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rankDifference =
|
|
51
|
+
(a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER);
|
|
52
|
+
if (rankDifference !== 0) {
|
|
53
|
+
return rankDifference;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return a.name.localeCompare(b.name);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getLatestMatchIdFromDaily = (daily: DailyChartRow[]) =>
|
|
60
|
+
daily.reduce((latest, row) => {
|
|
61
|
+
const match = row.day.match(/^Match\s+(\d+)$/i);
|
|
62
|
+
const matchId = match ? Number(match[1]) : Number.NaN;
|
|
63
|
+
|
|
64
|
+
return Number.isFinite(matchId) ? Math.max(latest, matchId) : latest;
|
|
65
|
+
}, 0);
|
|
66
|
+
|
|
67
|
+
const findRawUserIndex = (
|
|
68
|
+
users: RawApiUser[],
|
|
69
|
+
scrapedName: string,
|
|
70
|
+
aliases: Record<string, string> | undefined,
|
|
71
|
+
) => {
|
|
72
|
+
const target = normalizeTeamName(resolveTeamName(scrapedName, aliases));
|
|
73
|
+
return users.findIndex((user) => normalizeTeamName(user.temname) === target);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const normalizeSquadPlayer = (value: unknown): ScrapedSquadPlayer | null => {
|
|
77
|
+
if (typeof value === "string") {
|
|
78
|
+
const name = value.trim();
|
|
79
|
+
return name ? { name } : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!value || typeof value !== "object") {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const item = value as Record<string, unknown>;
|
|
87
|
+
const rawName = [item.name, item.playerName, item.fullName].find(
|
|
88
|
+
(entry): entry is string =>
|
|
89
|
+
typeof entry === "string" && entry.trim().length > 0,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (!rawName) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rawNumber = [item.number, item.id, item.playerId].find(
|
|
97
|
+
(entry): entry is string | number =>
|
|
98
|
+
(typeof entry === "string" && entry.trim().length > 0) ||
|
|
99
|
+
typeof entry === "number",
|
|
100
|
+
);
|
|
101
|
+
const rawPoints = [item.points, item.playerPoints, item.pts].find(
|
|
102
|
+
(entry) => entry !== undefined && entry !== null,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
name: rawName.trim(),
|
|
107
|
+
number: rawNumber === undefined ? undefined : String(rawNumber).trim(),
|
|
108
|
+
points: toFiniteNumber(rawPoints),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const toFiniteNumber = (value: unknown) => {
|
|
113
|
+
const numeric =
|
|
114
|
+
typeof value === "string"
|
|
115
|
+
? Number(value.replace(/,/g, ""))
|
|
116
|
+
: typeof value === "number"
|
|
117
|
+
? value
|
|
118
|
+
: Number.NaN;
|
|
119
|
+
|
|
120
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const addLeaderboardMetrics = (
|
|
124
|
+
leaders: ScrapedLeaderboardItem[],
|
|
125
|
+
options: TransformOptions = {},
|
|
126
|
+
): OverallChartItem[] => {
|
|
127
|
+
const totalTransfers = options.totalTransfers ?? DEFAULT_TOTAL_TRANSFERS;
|
|
128
|
+
const sorted = [...leaders]
|
|
129
|
+
.filter((leader) => Number.isFinite(leader.points))
|
|
130
|
+
.sort(compareLeadersByPoints);
|
|
131
|
+
const previousRankMap = new Map(
|
|
132
|
+
sorted
|
|
133
|
+
.map((leader) => ({
|
|
134
|
+
name: leader.name,
|
|
135
|
+
previousPoints:
|
|
136
|
+
leader.netPoints ?? leader.points - (leader.lastMatchPoints ?? 0),
|
|
137
|
+
}))
|
|
138
|
+
.sort((a, b) => b.previousPoints - a.previousPoints)
|
|
139
|
+
.map((leader, index) => [leader.name, index + 1]),
|
|
140
|
+
);
|
|
141
|
+
const maxLastMatchPoints = Math.max(
|
|
142
|
+
...sorted.map((leader) => leader.lastMatchPoints ?? 0),
|
|
143
|
+
);
|
|
144
|
+
const leaderboardRange = sorted.length
|
|
145
|
+
? sorted[0].points - sorted[sorted.length - 1].points
|
|
146
|
+
: 0;
|
|
147
|
+
|
|
148
|
+
return sorted.map((leader, index) => {
|
|
149
|
+
const rank = index + 1;
|
|
150
|
+
const previousRank = leader.netRank ?? previousRankMap.get(leader.name);
|
|
151
|
+
const nextLeader = sorted[index + 1];
|
|
152
|
+
const gapToNext = nextLeader ? leader.points - nextLeader.points : 0;
|
|
153
|
+
const usedTransfers =
|
|
154
|
+
typeof leader.transfersLeft === "number"
|
|
155
|
+
? totalTransfers - leader.transfersLeft
|
|
156
|
+
: 0;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
name: leader.name,
|
|
160
|
+
points: leader.points,
|
|
161
|
+
rank,
|
|
162
|
+
previousRank,
|
|
163
|
+
previousPoints:
|
|
164
|
+
leader.netPoints ?? leader.points - (leader.lastMatchPoints ?? 0),
|
|
165
|
+
lastMatchPoints: leader.lastMatchPoints,
|
|
166
|
+
gapToNext,
|
|
167
|
+
gapPercent: leaderboardRange ? (gapToNext / leaderboardRange) * 100 : 0,
|
|
168
|
+
movement:
|
|
169
|
+
previousRank === undefined
|
|
170
|
+
? "new"
|
|
171
|
+
: rank < previousRank
|
|
172
|
+
? "up"
|
|
173
|
+
: rank > previousRank
|
|
174
|
+
? "down"
|
|
175
|
+
: "same",
|
|
176
|
+
transfersLeft: leader.transfersLeft,
|
|
177
|
+
transfersUsed: leader.transfersUsed,
|
|
178
|
+
totalTransfers: leader.totalTransfers,
|
|
179
|
+
boostersUsed: leader.boostersUsed,
|
|
180
|
+
efficiency:
|
|
181
|
+
usedTransfers > 0
|
|
182
|
+
? Number((leader.points / usedTransfers).toFixed(2))
|
|
183
|
+
: undefined,
|
|
184
|
+
isLastMatchLeader:
|
|
185
|
+
(leader.lastMatchPoints ?? 0) > 0 &&
|
|
186
|
+
leader.lastMatchPoints === maxLastMatchPoints,
|
|
187
|
+
captain: leader.captain,
|
|
188
|
+
viceCaptain: leader.viceCaptain,
|
|
189
|
+
players: leader.players,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const buildManualDashboard = (
|
|
195
|
+
users: RawApiUser[],
|
|
196
|
+
options: TransformOptions = {},
|
|
197
|
+
): DashboardData => {
|
|
198
|
+
const latestMatchId = users.reduce((latest, user) => {
|
|
199
|
+
const userLatest = user.matches.reduce(
|
|
200
|
+
(max, match) => Math.max(max, match.matchId),
|
|
201
|
+
0,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return Math.max(latest, userLatest);
|
|
205
|
+
}, 0);
|
|
206
|
+
const sortedByPoints = [...users].sort((a, b) => b.points - a.points);
|
|
207
|
+
const leaders = sortedByPoints.map((user, index) => ({
|
|
208
|
+
rank: index + 1,
|
|
209
|
+
name: user.temname,
|
|
210
|
+
points: user.points,
|
|
211
|
+
lastMatchPoints:
|
|
212
|
+
user.matches.find((match) => match.matchId === latestMatchId)?.points ??
|
|
213
|
+
0,
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
const daily: DailyChartRow[] = Array.from(
|
|
217
|
+
{ length: latestMatchId },
|
|
218
|
+
(_, i) => {
|
|
219
|
+
const matchId = i + 1;
|
|
220
|
+
const matchScores = users
|
|
221
|
+
.map((user) => ({
|
|
222
|
+
team: user.temname,
|
|
223
|
+
points:
|
|
224
|
+
user.matches.find((match) => match.matchId === matchId)?.points ??
|
|
225
|
+
0,
|
|
226
|
+
}))
|
|
227
|
+
.sort((a, b) => b.points - a.points);
|
|
228
|
+
const row: DailyChartRow = { day: `Match ${matchId}` };
|
|
229
|
+
|
|
230
|
+
matchScores.forEach(({ team, points }) => {
|
|
231
|
+
row[team] = points;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return row;
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
overall: addLeaderboardMetrics(leaders, options),
|
|
240
|
+
daily,
|
|
241
|
+
completedMatches: latestMatchId || undefined,
|
|
242
|
+
source: "database",
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const normalizePayload = (
|
|
247
|
+
payload: unknown,
|
|
248
|
+
options: TransformOptions = {},
|
|
249
|
+
): ScrapedDashboardPayload | null => {
|
|
250
|
+
if (!payload || typeof payload !== "object") {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const payloadObject = payload as {
|
|
255
|
+
leaders?: unknown;
|
|
256
|
+
completedMatches?: unknown;
|
|
257
|
+
updatedAt?: unknown;
|
|
258
|
+
dailyTransferUpdatedAt?: unknown;
|
|
259
|
+
match?: unknown;
|
|
260
|
+
time?: unknown;
|
|
261
|
+
Data?: {
|
|
262
|
+
Value?: unknown;
|
|
263
|
+
FeedTime?: {
|
|
264
|
+
UTCTime?: unknown;
|
|
265
|
+
ISTTime?: unknown;
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
Meta?: {
|
|
269
|
+
Timestamp?: {
|
|
270
|
+
UTCTime?: unknown;
|
|
271
|
+
ISTTime?: unknown;
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const leaders = Array.isArray(payload)
|
|
277
|
+
? payload
|
|
278
|
+
: Array.isArray(payloadObject.leaders)
|
|
279
|
+
? payloadObject.leaders
|
|
280
|
+
: Array.isArray(payloadObject.Data?.Value)
|
|
281
|
+
? payloadObject.Data.Value
|
|
282
|
+
: undefined;
|
|
283
|
+
|
|
284
|
+
if (!Array.isArray(leaders)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const normalized = leaders
|
|
289
|
+
.map((leader): ScrapedLeaderboardItem | null => {
|
|
290
|
+
if (!leader || typeof leader !== "object") {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const item = leader as Record<string, unknown>;
|
|
295
|
+
const rank = toFiniteNumber(item.rank);
|
|
296
|
+
const points = toFiniteNumber(item.points);
|
|
297
|
+
const nameCandidates = [item.name, item.temname, item.usrname].filter(
|
|
298
|
+
(value): value is string => typeof value === "string",
|
|
299
|
+
);
|
|
300
|
+
const name =
|
|
301
|
+
nameCandidates
|
|
302
|
+
.map((value) => value.trim())
|
|
303
|
+
.find(Boolean)
|
|
304
|
+
?.trim() ?? "";
|
|
305
|
+
const lastMatchPoints = toFiniteNumber(item.lastMatchPoints ?? item.last);
|
|
306
|
+
const netPoints = toFiniteNumber(item.netPoints);
|
|
307
|
+
const netRank = toFiniteNumber(item.netRank);
|
|
308
|
+
const transfersLeft = toFiniteNumber(item.transfersLeft);
|
|
309
|
+
const transfersUsed = toFiniteNumber(item.transfersUsed);
|
|
310
|
+
const totalTransfers = toFiniteNumber(item.totalTransfers);
|
|
311
|
+
const captain = normalizeSquadPlayer(item.captain ?? item.c);
|
|
312
|
+
const viceCaptain = normalizeSquadPlayer(item.viceCaptain ?? item.v);
|
|
313
|
+
const captainPoints = toFiniteNumber(item.cPoints);
|
|
314
|
+
const viceCaptainPoints = toFiniteNumber(item.vPoints);
|
|
315
|
+
const boostersValue = [item.boostersUsed, item.boosters].find(
|
|
316
|
+
(entry) => entry !== undefined && entry !== null,
|
|
317
|
+
);
|
|
318
|
+
const players = Array.isArray(item.players)
|
|
319
|
+
? item.players
|
|
320
|
+
.map((player) => normalizeSquadPlayer(player))
|
|
321
|
+
.filter((player): player is NonNullable<typeof player> =>
|
|
322
|
+
Boolean(player),
|
|
323
|
+
)
|
|
324
|
+
: undefined;
|
|
325
|
+
|
|
326
|
+
if (!name || points === undefined || rank === undefined) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
rank: rank ?? Number.MAX_SAFE_INTEGER,
|
|
332
|
+
name: resolveTeamName(name, options.teamAliases),
|
|
333
|
+
points,
|
|
334
|
+
lastMatchPoints,
|
|
335
|
+
netPoints,
|
|
336
|
+
netRank,
|
|
337
|
+
transfersLeft,
|
|
338
|
+
transfersUsed,
|
|
339
|
+
totalTransfers,
|
|
340
|
+
boostersUsed:
|
|
341
|
+
typeof boostersValue === "string"
|
|
342
|
+
? boostersValue.trim()
|
|
343
|
+
: typeof boostersValue === "number" &&
|
|
344
|
+
Number.isFinite(boostersValue)
|
|
345
|
+
? String(boostersValue)
|
|
346
|
+
: undefined,
|
|
347
|
+
captain:
|
|
348
|
+
captain && captainPoints !== undefined
|
|
349
|
+
? { ...captain, points: captainPoints }
|
|
350
|
+
: captain,
|
|
351
|
+
viceCaptain:
|
|
352
|
+
viceCaptain && viceCaptainPoints !== undefined
|
|
353
|
+
? { ...viceCaptain, points: viceCaptainPoints }
|
|
354
|
+
: viceCaptain,
|
|
355
|
+
players,
|
|
356
|
+
};
|
|
357
|
+
})
|
|
358
|
+
.filter((leader): leader is ScrapedLeaderboardItem => Boolean(leader))
|
|
359
|
+
.sort(compareLeadersByPoints)
|
|
360
|
+
.map((leader, index) => ({
|
|
361
|
+
...leader,
|
|
362
|
+
rank: index + 1,
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
const updatedAtValue = [
|
|
366
|
+
payloadObject.updatedAt,
|
|
367
|
+
payloadObject.dailyTransferUpdatedAt,
|
|
368
|
+
payloadObject.time,
|
|
369
|
+
payloadObject.Data?.FeedTime?.UTCTime,
|
|
370
|
+
payloadObject.Data?.FeedTime?.ISTTime,
|
|
371
|
+
payloadObject.Meta?.Timestamp?.UTCTime,
|
|
372
|
+
payloadObject.Meta?.Timestamp?.ISTTime,
|
|
373
|
+
].find((value) => value !== undefined && value !== null);
|
|
374
|
+
const completedMatches = [
|
|
375
|
+
payloadObject.completedMatches,
|
|
376
|
+
payloadObject.match,
|
|
377
|
+
String(payloadObject.Data?.FeedTime?.UTCTime ?? "").match(
|
|
378
|
+
/Match\s*(\d{1,2})/i,
|
|
379
|
+
)?.[1],
|
|
380
|
+
]
|
|
381
|
+
.map((value) => toFiniteNumber(value))
|
|
382
|
+
.find((value) => value !== undefined);
|
|
383
|
+
|
|
384
|
+
const updatedAt =
|
|
385
|
+
typeof updatedAtValue === "string" &&
|
|
386
|
+
!Number.isNaN(Date.parse(updatedAtValue))
|
|
387
|
+
? new Date(updatedAtValue).toISOString()
|
|
388
|
+
: updatedAtValue instanceof Date &&
|
|
389
|
+
!Number.isNaN(updatedAtValue.getTime())
|
|
390
|
+
? updatedAtValue.toISOString()
|
|
391
|
+
: new Date().toISOString();
|
|
392
|
+
|
|
393
|
+
return normalized.length
|
|
394
|
+
? {
|
|
395
|
+
updatedAt,
|
|
396
|
+
dailyTransferUpdatedAt:
|
|
397
|
+
typeof payloadObject.dailyTransferUpdatedAt === "string" &&
|
|
398
|
+
!Number.isNaN(Date.parse(payloadObject.dailyTransferUpdatedAt))
|
|
399
|
+
? new Date(payloadObject.dailyTransferUpdatedAt).toISOString()
|
|
400
|
+
: undefined,
|
|
401
|
+
completedMatches:
|
|
402
|
+
completedMatches !== undefined && completedMatches > 0
|
|
403
|
+
? Math.floor(completedMatches)
|
|
404
|
+
: undefined,
|
|
405
|
+
leaders: normalized,
|
|
406
|
+
}
|
|
407
|
+
: null;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const normalizeRawApiUsers = (payload: unknown): RawApiUser[] | null => {
|
|
411
|
+
if (!Array.isArray(payload)) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const normalized = payload
|
|
416
|
+
.map((user): RawApiUser | null => {
|
|
417
|
+
if (!user || typeof user !== "object") {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const item = user as Record<string, unknown>;
|
|
422
|
+
const rno = toFiniteNumber(item.rno);
|
|
423
|
+
const temname =
|
|
424
|
+
typeof item.temname === "string" ? item.temname.trim() : "";
|
|
425
|
+
const matches = Array.isArray(item.matches)
|
|
426
|
+
? item.matches
|
|
427
|
+
.map((match) => {
|
|
428
|
+
if (!match || typeof match !== "object") {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const matchItem = match as Record<string, unknown>;
|
|
433
|
+
const matchId = toFiniteNumber(matchItem.matchId);
|
|
434
|
+
const points = toFiniteNumber(matchItem.points);
|
|
435
|
+
|
|
436
|
+
if (matchId === undefined || points === undefined) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { matchId, points };
|
|
441
|
+
})
|
|
442
|
+
.filter(
|
|
443
|
+
(
|
|
444
|
+
match,
|
|
445
|
+
): match is {
|
|
446
|
+
matchId: number;
|
|
447
|
+
points: number;
|
|
448
|
+
} => Boolean(match),
|
|
449
|
+
)
|
|
450
|
+
.sort((a, b) => a.matchId - b.matchId)
|
|
451
|
+
: [];
|
|
452
|
+
|
|
453
|
+
if (rno === undefined || !temname || !matches.length) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
rno,
|
|
459
|
+
temname,
|
|
460
|
+
matches,
|
|
461
|
+
points: matches.reduce((sum, match) => sum + match.points, 0),
|
|
462
|
+
};
|
|
463
|
+
})
|
|
464
|
+
.filter((user): user is RawApiUser => Boolean(user))
|
|
465
|
+
.sort((a, b) => a.rno - b.rno);
|
|
466
|
+
|
|
467
|
+
return normalized.length ? normalized : null;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
export const buildDashboardFromSnapshot = (
|
|
471
|
+
snapshot: ScrapedDashboardPayload,
|
|
472
|
+
manualDashboard: DashboardData,
|
|
473
|
+
options: TransformOptions = {},
|
|
474
|
+
): DashboardData => {
|
|
475
|
+
const liveDaily: DailyChartRow | null = snapshot.leaders.some(
|
|
476
|
+
(leader) => typeof leader.lastMatchPoints === "number",
|
|
477
|
+
)
|
|
478
|
+
? snapshot.leaders.reduce<DailyChartRow>(
|
|
479
|
+
(row, leader) => {
|
|
480
|
+
row[leader.name] = leader.lastMatchPoints ?? 0;
|
|
481
|
+
return row;
|
|
482
|
+
},
|
|
483
|
+
{ day: "Live Update" },
|
|
484
|
+
)
|
|
485
|
+
: null;
|
|
486
|
+
const manualCompletedMatches =
|
|
487
|
+
manualDashboard.completedMatches ??
|
|
488
|
+
getLatestMatchIdFromDaily(manualDashboard.daily);
|
|
489
|
+
const completedMatches =
|
|
490
|
+
typeof snapshot.completedMatches === "number" &&
|
|
491
|
+
snapshot.completedMatches > 0
|
|
492
|
+
? snapshot.completedMatches
|
|
493
|
+
: liveDaily
|
|
494
|
+
? (manualCompletedMatches ?? 0) + 1
|
|
495
|
+
: manualCompletedMatches;
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
overall: addLeaderboardMetrics(snapshot.leaders, options),
|
|
499
|
+
daily: liveDaily
|
|
500
|
+
? [...manualDashboard.daily, liveDaily]
|
|
501
|
+
: manualDashboard.daily,
|
|
502
|
+
updatedAt: snapshot.updatedAt,
|
|
503
|
+
dailyTransferUpdatedAt: snapshot.dailyTransferUpdatedAt,
|
|
504
|
+
completedMatches,
|
|
505
|
+
source: "database",
|
|
506
|
+
snapshot,
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export const syncRawUsersWithSnapshot = (
|
|
511
|
+
users: RawApiUser[],
|
|
512
|
+
snapshot: ScrapedDashboardPayload,
|
|
513
|
+
options: TransformOptions = {},
|
|
514
|
+
): RawUsersSyncResult => {
|
|
515
|
+
const clonedUsers = users.map((user) => ({
|
|
516
|
+
...user,
|
|
517
|
+
matches: user.matches.map((match) => ({ ...match })),
|
|
518
|
+
}));
|
|
519
|
+
const maxMatchId = clonedUsers.reduce(
|
|
520
|
+
(max, user) => Math.max(max, ...user.matches.map((match) => match.matchId)),
|
|
521
|
+
0,
|
|
522
|
+
);
|
|
523
|
+
const completedMatches =
|
|
524
|
+
typeof snapshot.completedMatches === "number" &&
|
|
525
|
+
Number.isFinite(snapshot.completedMatches) &&
|
|
526
|
+
snapshot.completedMatches > 0
|
|
527
|
+
? Math.floor(snapshot.completedMatches)
|
|
528
|
+
: undefined;
|
|
529
|
+
const unmatchedNames: string[] = [];
|
|
530
|
+
const updates = snapshot.leaders.flatMap((leader) => {
|
|
531
|
+
const userIndex = findRawUserIndex(
|
|
532
|
+
clonedUsers,
|
|
533
|
+
leader.name,
|
|
534
|
+
options.teamAliases,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
if (userIndex < 0) {
|
|
538
|
+
unmatchedNames.push(leader.name);
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const user = clonedUsers[userIndex];
|
|
543
|
+
const currentTotal = calculateTotalPoints(user);
|
|
544
|
+
const delta = roundPoint(leader.points - currentTotal);
|
|
545
|
+
const latestMatch = user.matches.find(
|
|
546
|
+
(match) => match.matchId === maxMatchId,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
return [
|
|
550
|
+
{
|
|
551
|
+
userIndex,
|
|
552
|
+
delta,
|
|
553
|
+
latestMatchPoints: leader.lastMatchPoints,
|
|
554
|
+
currentLatestPoints: latestMatch?.points ?? 0,
|
|
555
|
+
},
|
|
556
|
+
];
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if (!updates.length) {
|
|
560
|
+
return {
|
|
561
|
+
status: "skipped",
|
|
562
|
+
users: clonedUsers,
|
|
563
|
+
unmatchedNames,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const changedUpdates = updates.filter(
|
|
568
|
+
(update) => Math.abs(update.delta) > FLOAT_TOLERANCE,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
if (!changedUpdates.length) {
|
|
572
|
+
return {
|
|
573
|
+
status: "unchanged",
|
|
574
|
+
users: clonedUsers,
|
|
575
|
+
matchId: maxMatchId,
|
|
576
|
+
completedMatches,
|
|
577
|
+
unmatchedNames,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const hasFutureReportedMatch =
|
|
582
|
+
typeof completedMatches === "number" && completedMatches > maxMatchId;
|
|
583
|
+
const shouldForceUpdateLatest =
|
|
584
|
+
typeof completedMatches === "number" && completedMatches === maxMatchId;
|
|
585
|
+
const updatesFitLatestMatch = changedUpdates.every((update) => {
|
|
586
|
+
if (typeof update.latestMatchPoints !== "number") {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
Math.abs(
|
|
592
|
+
update.currentLatestPoints + update.delta - update.latestMatchPoints,
|
|
593
|
+
) <= FLOAT_TOLERANCE
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
const mode =
|
|
597
|
+
(updatesFitLatestMatch || shouldForceUpdateLatest) &&
|
|
598
|
+
!hasFutureReportedMatch
|
|
599
|
+
? "update-latest"
|
|
600
|
+
: "append";
|
|
601
|
+
const matchId =
|
|
602
|
+
mode === "append"
|
|
603
|
+
? Math.max(maxMatchId + 1, completedMatches ?? maxMatchId + 1)
|
|
604
|
+
: maxMatchId;
|
|
605
|
+
|
|
606
|
+
changedUpdates.forEach((update) => {
|
|
607
|
+
const user = clonedUsers[update.userIndex];
|
|
608
|
+
|
|
609
|
+
if (mode === "update-latest") {
|
|
610
|
+
const latestMatch = user.matches.find(
|
|
611
|
+
(match) => match.matchId === matchId,
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
if (latestMatch) {
|
|
615
|
+
latestMatch.points = roundPoint(latestMatch.points + update.delta);
|
|
616
|
+
} else {
|
|
617
|
+
user.matches.push({ matchId, points: update.delta });
|
|
618
|
+
}
|
|
619
|
+
} else if (matchId > maxMatchId + 1) {
|
|
620
|
+
const gapStart = maxMatchId + 1;
|
|
621
|
+
const latestPoints =
|
|
622
|
+
typeof update.latestMatchPoints === "number"
|
|
623
|
+
? roundPoint(update.latestMatchPoints)
|
|
624
|
+
: undefined;
|
|
625
|
+
const catchupPoints =
|
|
626
|
+
latestPoints !== undefined
|
|
627
|
+
? roundPoint(update.delta - latestPoints)
|
|
628
|
+
: roundPoint(update.delta);
|
|
629
|
+
|
|
630
|
+
user.matches.push({ matchId: gapStart, points: catchupPoints });
|
|
631
|
+
|
|
632
|
+
for (let id = gapStart + 1; id < matchId; id += 1) {
|
|
633
|
+
user.matches.push({ matchId: id, points: 0 });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (latestPoints !== undefined) {
|
|
637
|
+
user.matches.push({ matchId, points: latestPoints });
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
user.matches.push({ matchId, points: update.delta });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
user.matches.sort((a, b) => a.matchId - b.matchId);
|
|
644
|
+
user.points = calculateTotalPoints(user);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
status: "updated",
|
|
649
|
+
users: clonedUsers,
|
|
650
|
+
matchId,
|
|
651
|
+
mode,
|
|
652
|
+
completedMatches,
|
|
653
|
+
unmatchedNames,
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
export const serializeRawApiUsersModule = (users: RawApiUser[]) => {
|
|
658
|
+
const usersForSource = users.map((user) => ({
|
|
659
|
+
rno: user.rno,
|
|
660
|
+
temname: user.temname,
|
|
661
|
+
points: 0,
|
|
662
|
+
matches: [...user.matches].sort((a, b) => a.matchId - b.matchId),
|
|
663
|
+
}));
|
|
664
|
+
|
|
665
|
+
return `export type RawApiUser = {
|
|
666
|
+
rno: number;
|
|
667
|
+
temname: string;
|
|
668
|
+
points: number;
|
|
669
|
+
matches: Array<{
|
|
670
|
+
matchId: number;
|
|
671
|
+
points: number;
|
|
672
|
+
}>;
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const calculateTotalPoints = (
|
|
676
|
+
matches: Array<{ matchId: number; points: number }>,
|
|
677
|
+
): number => {
|
|
678
|
+
return matches.reduce((sum, match) => sum + match.points, 0);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
export const rawApiUsers: RawApiUser[] = ${JSON.stringify(usersForSource, null, 2)}
|
|
682
|
+
.map((user) => ({
|
|
683
|
+
...user,
|
|
684
|
+
points: calculateTotalPoints(user.matches),
|
|
685
|
+
}));
|
|
686
|
+
`;
|
|
687
|
+
};
|