@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,257 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { DashboardData } from "../../types";
|
|
3
|
+
import {
|
|
4
|
+
getDisplayLiveMatchId,
|
|
5
|
+
getPlayedMatchRows,
|
|
6
|
+
getNumericRowEntries,
|
|
7
|
+
} from "../../lib/dashboardData";
|
|
8
|
+
import { getMatchViewState } from "../../lib/dashboardView";
|
|
9
|
+
import { buildTeamBadgeMap } from "../../lib/teamBadges";
|
|
10
|
+
import { buildDetailedTableRows } from "../../lib/detailedData";
|
|
11
|
+
import type { DashboardModel } from "../../types/dashboard";
|
|
12
|
+
import { formatCompactNumber } from "../../utils/dashboard";
|
|
13
|
+
import {
|
|
14
|
+
FaTrophy,
|
|
15
|
+
FaArrowTrendUp,
|
|
16
|
+
FaBolt,
|
|
17
|
+
FaArrowTrendDown,
|
|
18
|
+
FaCircle,
|
|
19
|
+
FaMinus,
|
|
20
|
+
} from "react-icons/fa6";
|
|
21
|
+
|
|
22
|
+
const BRIGHT_COLORS = {
|
|
23
|
+
yellow: "#eab308",
|
|
24
|
+
green: "#84cc16",
|
|
25
|
+
pink: "#ec4899",
|
|
26
|
+
orange: "#f97316",
|
|
27
|
+
blue: "#3b82f6",
|
|
28
|
+
purple: "#a855f7",
|
|
29
|
+
cyan: "#22d3ee",
|
|
30
|
+
amber: "#f59e0b",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export function buildDashboardModel(
|
|
34
|
+
data: DashboardData | null,
|
|
35
|
+
): DashboardModel | null {
|
|
36
|
+
if (!data?.overall?.length) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const overall = [...data.overall].sort((a, b) => a.rank - b.rank);
|
|
41
|
+
const normalizedOverall = overall.map((team, index) => {
|
|
42
|
+
const nextTeam = overall[index + 1];
|
|
43
|
+
const gapToNext =
|
|
44
|
+
typeof nextTeam?.points === "number"
|
|
45
|
+
? Math.max(0, team.points - nextTeam.points)
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...team,
|
|
50
|
+
gapToNext: team.gapToNext ?? gapToNext,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const playedMatches = getPlayedMatchRows(data.daily);
|
|
55
|
+
const matchView = getMatchViewState(data.daily);
|
|
56
|
+
const liveEntries = matchView.latestRow
|
|
57
|
+
? getNumericRowEntries(matchView.latestRow).sort(
|
|
58
|
+
(a, b) => b.points - a.points,
|
|
59
|
+
)
|
|
60
|
+
: [];
|
|
61
|
+
|
|
62
|
+
const leader = normalizedOverall[0];
|
|
63
|
+
const chaser = normalizedOverall[1];
|
|
64
|
+
const woodenSpoon = normalizedOverall.at(-1);
|
|
65
|
+
const mostTransfers = [...normalizedOverall]
|
|
66
|
+
.filter((team) => typeof team.transfersLeft === "number")
|
|
67
|
+
.sort((a, b) => (b.transfersLeft ?? 0) - (a.transfersLeft ?? 0))[0];
|
|
68
|
+
const hottest = [...normalizedOverall].sort(
|
|
69
|
+
(a, b) => (b.lastMatchPoints ?? 0) - (a.lastMatchPoints ?? 0),
|
|
70
|
+
)[0];
|
|
71
|
+
|
|
72
|
+
const topTeams = normalizedOverall.slice(0, 8);
|
|
73
|
+
const barLeaderboard = normalizedOverall.slice(0, 8).map((team) => ({
|
|
74
|
+
label: team.name,
|
|
75
|
+
value: team.points,
|
|
76
|
+
sublabel: `#${team.rank}`,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const liveBars = liveEntries.slice(0, 8).map((entry) => ({
|
|
80
|
+
label: entry.team,
|
|
81
|
+
value: entry.points,
|
|
82
|
+
sublabel: matchView.isLive
|
|
83
|
+
? "live"
|
|
84
|
+
: (matchView.latestRow?.day ?? "latest"),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const captainTeams = normalizedOverall.slice(0, 8);
|
|
88
|
+
const latestDailyRow = matchView.latestRow;
|
|
89
|
+
const tableRows = buildDetailedTableRows({
|
|
90
|
+
overall: normalizedOverall,
|
|
91
|
+
latestDailyRow,
|
|
92
|
+
snapshotLeaders: data.snapshot?.leaders,
|
|
93
|
+
transfers: data.transfers,
|
|
94
|
+
}).slice(0, 8);
|
|
95
|
+
|
|
96
|
+
const teamBadges = buildTeamBadgeMap(data);
|
|
97
|
+
const topTeamsChart = normalizedOverall.slice(0, 8).map((team) => ({
|
|
98
|
+
label: team.name,
|
|
99
|
+
value: team.points,
|
|
100
|
+
sublabel: `#${team.rank}`,
|
|
101
|
+
difference: typeof team.gapToNext === "number" ? team.gapToNext : undefined,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
const insights = createInsights(
|
|
105
|
+
leader,
|
|
106
|
+
chaser,
|
|
107
|
+
mostTransfers,
|
|
108
|
+
woodenSpoon,
|
|
109
|
+
liveEntries,
|
|
110
|
+
matchView,
|
|
111
|
+
);
|
|
112
|
+
const summaryCards = createSummaryCards(
|
|
113
|
+
leader,
|
|
114
|
+
hottest,
|
|
115
|
+
mostTransfers,
|
|
116
|
+
woodenSpoon,
|
|
117
|
+
chaser,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
leader,
|
|
122
|
+
topTeams,
|
|
123
|
+
normalizedOverall,
|
|
124
|
+
teamBadges,
|
|
125
|
+
insights,
|
|
126
|
+
summaryCards,
|
|
127
|
+
barLeaderboard,
|
|
128
|
+
liveBars,
|
|
129
|
+
topTeamsChart,
|
|
130
|
+
captainTeams,
|
|
131
|
+
tableRows,
|
|
132
|
+
liveLeader: liveEntries[0],
|
|
133
|
+
liveLabel: matchView.isLive
|
|
134
|
+
? `Match ${getDisplayLiveMatchId(data.daily)} live`
|
|
135
|
+
: playedMatches.length
|
|
136
|
+
? `Match ${getDisplayLiveMatchId(data.daily)} recap`
|
|
137
|
+
: "Live page",
|
|
138
|
+
liveChartTitle: matchView.isLive
|
|
139
|
+
? `Match ${getDisplayLiveMatchId(data.daily)} Pulse`
|
|
140
|
+
: playedMatches.length
|
|
141
|
+
? `Match ${getDisplayLiveMatchId(data.daily)} Recap`
|
|
142
|
+
: "Latest Match Pulse",
|
|
143
|
+
matchCount: playedMatches.length,
|
|
144
|
+
hottest,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createInsights(
|
|
149
|
+
leader: any,
|
|
150
|
+
chaser: any,
|
|
151
|
+
mostTransfers: any,
|
|
152
|
+
woodenSpoon: any,
|
|
153
|
+
liveEntries: any[],
|
|
154
|
+
matchView: any,
|
|
155
|
+
) {
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
id: "leader-gap",
|
|
159
|
+
title: "Big circle around first place",
|
|
160
|
+
note: chaser
|
|
161
|
+
? `${leader.name} leads ${chaser.name} by ${(leader.points - chaser.points).toLocaleString()} points.`
|
|
162
|
+
: `${leader.name} is alone at the top for now.`,
|
|
163
|
+
accent: BRIGHT_COLORS.yellow,
|
|
164
|
+
icon: FaTrophy,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "live-leader",
|
|
168
|
+
title: matchView.isLive ? "Live scribble" : "Last finished match",
|
|
169
|
+
note: liveEntries[0]
|
|
170
|
+
? `${liveEntries[0].team} owns the latest page with ${liveEntries[0].points.toLocaleString()} points.`
|
|
171
|
+
: "No live row yet, so the margin is waiting for the next burst.",
|
|
172
|
+
accent: BRIGHT_COLORS.green,
|
|
173
|
+
icon: FaArrowTrendUp,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "transfers",
|
|
177
|
+
title: "Sticky note on transfers",
|
|
178
|
+
note: mostTransfers
|
|
179
|
+
? `${mostTransfers.name} still has ${mostTransfers.transfersLeft} transfers tucked away.`
|
|
180
|
+
: "Transfer counts will appear once the snapshot includes them.",
|
|
181
|
+
accent: BRIGHT_COLORS.pink,
|
|
182
|
+
icon: FaBolt,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "dramatic",
|
|
186
|
+
title: "Teacher red pen",
|
|
187
|
+
note: woodenSpoon
|
|
188
|
+
? `${woodenSpoon.name} is on cleanup duty at rank #${woodenSpoon.rank}, but one wild match can rewrite the page.`
|
|
189
|
+
: "The bottom of the table is still being drafted.",
|
|
190
|
+
accent: BRIGHT_COLORS.orange,
|
|
191
|
+
icon: FaArrowTrendDown,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createSummaryCards(
|
|
197
|
+
leader: any,
|
|
198
|
+
hottest: any,
|
|
199
|
+
mostTransfers: any,
|
|
200
|
+
woodenSpoon: any,
|
|
201
|
+
chaser: any,
|
|
202
|
+
) {
|
|
203
|
+
return [
|
|
204
|
+
{
|
|
205
|
+
id: "leader-summary",
|
|
206
|
+
title: "Leader",
|
|
207
|
+
value: `${leader.name}`,
|
|
208
|
+
note: `${formatCompactNumber(leader.points)} pts`,
|
|
209
|
+
accent: BRIGHT_COLORS.yellow,
|
|
210
|
+
icon: FaTrophy,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "hot-streak",
|
|
214
|
+
title: "Hottest",
|
|
215
|
+
value: `${hottest.name}`,
|
|
216
|
+
note: `${(hottest.lastMatchPoints ?? 0).toLocaleString()} pts`,
|
|
217
|
+
accent: BRIGHT_COLORS.green,
|
|
218
|
+
icon: FaArrowTrendUp,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "transfer-stock",
|
|
222
|
+
title: "Transfers",
|
|
223
|
+
value: mostTransfers ? `${mostTransfers.name}` : "—",
|
|
224
|
+
note: mostTransfers ? `${mostTransfers.transfersLeft} left` : "No data",
|
|
225
|
+
accent: BRIGHT_COLORS.pink,
|
|
226
|
+
icon: FaBolt,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "bottom-watch",
|
|
230
|
+
title: "Bottom",
|
|
231
|
+
value: woodenSpoon ? `${woodenSpoon.name}` : "—",
|
|
232
|
+
note: woodenSpoon ? `#${woodenSpoon.rank}` : "No data",
|
|
233
|
+
accent: BRIGHT_COLORS.orange,
|
|
234
|
+
icon: FaCircle,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "chaser",
|
|
238
|
+
title: "Close chase",
|
|
239
|
+
value: chaser ? `${chaser.name}` : "—",
|
|
240
|
+
note: chaser
|
|
241
|
+
? `${(leader.points - chaser.points).toLocaleString()} pts behind`
|
|
242
|
+
: "No chaser",
|
|
243
|
+
accent: BRIGHT_COLORS.blue,
|
|
244
|
+
icon: FaBolt,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "gap-to-last",
|
|
248
|
+
title: "Field spread",
|
|
249
|
+
value: woodenSpoon
|
|
250
|
+
? `${(leader.points - woodenSpoon.points).toLocaleString()} pts`
|
|
251
|
+
: "—",
|
|
252
|
+
note: "Leader vs bottom gap",
|
|
253
|
+
accent: BRIGHT_COLORS.purple,
|
|
254
|
+
icon: FaMinus,
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DailyChartRow } from "../types";
|
|
2
|
+
|
|
3
|
+
export const parseMatchId = (day: string) => {
|
|
4
|
+
const match = day.match(/^Match\s+(\d+)$/i);
|
|
5
|
+
const parsed = match ? Number(match[1]) : Number.NaN;
|
|
6
|
+
|
|
7
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const isLiveUpdateRow = (row: DailyChartRow) => row.day === "Live Update";
|
|
11
|
+
|
|
12
|
+
export const isMatchRow = (row: DailyChartRow) =>
|
|
13
|
+
typeof parseMatchId(row.day) === "number";
|
|
14
|
+
|
|
15
|
+
export const getNumericRowEntries = (row: DailyChartRow) =>
|
|
16
|
+
Object.entries(row)
|
|
17
|
+
.filter(([key]) => key !== "day")
|
|
18
|
+
.map(([team, value]) => ({
|
|
19
|
+
team,
|
|
20
|
+
points: Number(value ?? 0),
|
|
21
|
+
}))
|
|
22
|
+
.filter((entry) => Number.isFinite(entry.points));
|
|
23
|
+
|
|
24
|
+
export const hasMeaningfulMatchData = (row: DailyChartRow) =>
|
|
25
|
+
getNumericRowEntries(row).some((entry) => entry.points !== 0);
|
|
26
|
+
|
|
27
|
+
export const getLiveUpdateRow = (daily: DailyChartRow[]) =>
|
|
28
|
+
daily.find(isLiveUpdateRow);
|
|
29
|
+
|
|
30
|
+
export const getHistoricalMatchRows = (daily: DailyChartRow[]) =>
|
|
31
|
+
daily.filter(isMatchRow).sort((a, b) => {
|
|
32
|
+
const aId = parseMatchId(a.day) ?? 0;
|
|
33
|
+
const bId = parseMatchId(b.day) ?? 0;
|
|
34
|
+
|
|
35
|
+
return aId - bId;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const getPlayedMatchRows = (daily: DailyChartRow[]) =>
|
|
39
|
+
getHistoricalMatchRows(daily).filter(hasMeaningfulMatchData);
|
|
40
|
+
|
|
41
|
+
export const getLatestPlayedMatchId = (daily: DailyChartRow[]) =>
|
|
42
|
+
getPlayedMatchRows(daily).reduce((latest, row) => {
|
|
43
|
+
const matchId = parseMatchId(row.day) ?? 0;
|
|
44
|
+
return Math.max(latest, matchId);
|
|
45
|
+
}, 0);
|
|
46
|
+
|
|
47
|
+
export const getDisplayLiveMatchId = (daily: DailyChartRow[]) => {
|
|
48
|
+
const latestPlayedMatchId = getLatestPlayedMatchId(daily);
|
|
49
|
+
return getLiveUpdateRow(daily) ? latestPlayedMatchId + 1 : latestPlayedMatchId;
|
|
50
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DailyChartRow } from "../types";
|
|
2
|
+
import { getLiveUpdateRow, getPlayedMatchRows } from "./dashboardData.ts";
|
|
3
|
+
|
|
4
|
+
export const getLatestMatchRow = (daily: DailyChartRow[]) =>
|
|
5
|
+
getLiveUpdateRow(daily) ?? getPlayedMatchRows(daily).at(-1);
|
|
6
|
+
|
|
7
|
+
export const hasLiveMatchRow = (daily: DailyChartRow[]) =>
|
|
8
|
+
Boolean(getLiveUpdateRow(daily));
|
|
9
|
+
|
|
10
|
+
export const getMatchViewState = (daily: DailyChartRow[]) => {
|
|
11
|
+
const liveRow = getLiveUpdateRow(daily);
|
|
12
|
+
const playedRows = getPlayedMatchRows(daily);
|
|
13
|
+
const latestRow = liveRow ?? playedRows.at(-1);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
liveRow,
|
|
17
|
+
playedRows,
|
|
18
|
+
latestRow,
|
|
19
|
+
isLive: Boolean(liveRow),
|
|
20
|
+
hasMatches: playedRows.length > 0 || Boolean(liveRow),
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { config } from "./config.ts";
|
|
2
|
+
import type {
|
|
3
|
+
DashboardData,
|
|
4
|
+
OverallChartItem,
|
|
5
|
+
ScrapedLeaderboardItem,
|
|
6
|
+
} from "../types";
|
|
7
|
+
|
|
8
|
+
type DetailedTableRow = OverallChartItem & {
|
|
9
|
+
latestPoints?: number;
|
|
10
|
+
transferMatchesPlayed?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getSnapshotBoostersUsed = (leader: unknown) => {
|
|
14
|
+
if (!leader || typeof leader !== "object") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const item = leader as Record<string, unknown>;
|
|
19
|
+
const rawValue = item.boostersUsed ?? item.boosters;
|
|
20
|
+
|
|
21
|
+
if (typeof rawValue === "string") {
|
|
22
|
+
return rawValue.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
|
26
|
+
return String(rawValue);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const toNumber = (value: string | number | undefined) => {
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
return Number.isFinite(value) ? value : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof value === "string") {
|
|
38
|
+
const parsed = Number(value.trim());
|
|
39
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildTransferMap = (transfers?: DashboardData["transfers"]) => {
|
|
46
|
+
return new Map(
|
|
47
|
+
(transfers?.teams ?? []).map((team) => [team.team.toLowerCase(), team]),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const buildSnapshotLeaderMap = (leaders?: ScrapedLeaderboardItem[]) => {
|
|
52
|
+
return new Map((leaders ?? []).map((leader) => [leader.name, leader]));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const buildDetailedTableRows = ({
|
|
56
|
+
overall,
|
|
57
|
+
latestDailyRow,
|
|
58
|
+
snapshotLeaders,
|
|
59
|
+
transfers,
|
|
60
|
+
}: {
|
|
61
|
+
overall: OverallChartItem[];
|
|
62
|
+
latestDailyRow?: Record<string, string | number>;
|
|
63
|
+
snapshotLeaders?: ScrapedLeaderboardItem[];
|
|
64
|
+
transfers?: DashboardData["transfers"];
|
|
65
|
+
}): DetailedTableRow[] => {
|
|
66
|
+
const transferMap = buildTransferMap(transfers);
|
|
67
|
+
const snapshotLeaderMap = buildSnapshotLeaderMap(snapshotLeaders);
|
|
68
|
+
|
|
69
|
+
return [...overall]
|
|
70
|
+
.sort((a, b) => a.rank - b.rank)
|
|
71
|
+
.map((row) => {
|
|
72
|
+
const snapshotLeader = snapshotLeaderMap.get(row.name);
|
|
73
|
+
const transferRecord = transferMap.get(row.name.toLowerCase());
|
|
74
|
+
const latestDailyPoints = latestDailyRow?.[row.name];
|
|
75
|
+
const latestPoints =
|
|
76
|
+
typeof latestDailyPoints === "number"
|
|
77
|
+
? latestDailyPoints
|
|
78
|
+
: row.lastMatchPoints;
|
|
79
|
+
|
|
80
|
+
const transfersLeft =
|
|
81
|
+
toNumber(transferRecord?.transfersLeft) ??
|
|
82
|
+
row.transfersLeft ??
|
|
83
|
+
(typeof snapshotLeader?.transfersLeft === "number"
|
|
84
|
+
? snapshotLeader.transfersLeft
|
|
85
|
+
: undefined);
|
|
86
|
+
const totalTransfers = config.league.totalTransfers;
|
|
87
|
+
const transfersUsed =
|
|
88
|
+
typeof transfersLeft === "number"
|
|
89
|
+
? totalTransfers - transfersLeft
|
|
90
|
+
: undefined;
|
|
91
|
+
const efficiency =
|
|
92
|
+
typeof transfersUsed === "number" && transfersUsed > 0 && row.points > 0
|
|
93
|
+
? row.points / transfersUsed
|
|
94
|
+
: undefined;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...row,
|
|
98
|
+
latestPoints,
|
|
99
|
+
transfersLeft,
|
|
100
|
+
transfersUsed,
|
|
101
|
+
totalTransfers,
|
|
102
|
+
efficiency,
|
|
103
|
+
boostersUsed:
|
|
104
|
+
(typeof transferRecord?.boostersUsed === "number"
|
|
105
|
+
? String(transferRecord.boostersUsed)
|
|
106
|
+
: undefined) ??
|
|
107
|
+
row.boostersUsed ??
|
|
108
|
+
getSnapshotBoostersUsed(snapshotLeader),
|
|
109
|
+
transferMatchesPlayed: transferRecord?.matchesPlayed,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { matches } from "./matches";
|
|
2
|
+
|
|
3
|
+
export type MatchStatus = "ENDED" | "LIVE" | "SOON" | "UPCOMING";
|
|
4
|
+
|
|
5
|
+
export function getMatchStatus(date: string) {
|
|
6
|
+
const now = new Date().getTime();
|
|
7
|
+
const matchTime = new Date(date).getTime();
|
|
8
|
+
|
|
9
|
+
const diff = matchTime - now;
|
|
10
|
+
|
|
11
|
+
if (diff < -4 * 60 * 60 * 1000) return "ENDED" as const;
|
|
12
|
+
if (diff <= 0) return "LIVE" as const;
|
|
13
|
+
if (diff <= 30 * 60 * 1000) return "SOON" as const;
|
|
14
|
+
|
|
15
|
+
return "UPCOMING" as const;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getCurrentMatchStatus() {
|
|
19
|
+
const current = matches
|
|
20
|
+
.map((match) => ({
|
|
21
|
+
...match,
|
|
22
|
+
status: getMatchStatus(match.date),
|
|
23
|
+
}))
|
|
24
|
+
.filter((match) => match.status !== "ENDED")
|
|
25
|
+
.sort((a, b) => +new Date(a.date) - +new Date(b.date))[0];
|
|
26
|
+
|
|
27
|
+
return current?.status ?? "ENDED";
|
|
28
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
type Match = {
|
|
2
|
+
id: number;
|
|
3
|
+
team1: string;
|
|
4
|
+
team2: string;
|
|
5
|
+
date: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const matches: Match[] = [
|
|
9
|
+
{
|
|
10
|
+
id: 51,
|
|
11
|
+
team1: "DC",
|
|
12
|
+
team2: "KKR",
|
|
13
|
+
date: "2026-05-08T14:00:00Z",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 52,
|
|
17
|
+
team1: "RR",
|
|
18
|
+
team2: "GT",
|
|
19
|
+
date: "2026-05-09T14:00:00Z",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 53,
|
|
23
|
+
team1: "CSK",
|
|
24
|
+
team2: "LSG",
|
|
25
|
+
date: "2026-05-10T10:00:00Z",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 54,
|
|
29
|
+
team1: "RCB",
|
|
30
|
+
team2: "MI",
|
|
31
|
+
date: "2026-05-10T14:00:00Z",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 55,
|
|
35
|
+
team1: "PBKS",
|
|
36
|
+
team2: "DC",
|
|
37
|
+
date: "2026-05-11T14:00:00Z",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 56,
|
|
41
|
+
team1: "GT",
|
|
42
|
+
team2: "SRH",
|
|
43
|
+
date: "2026-05-12T14:00:00Z",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 57,
|
|
47
|
+
team1: "RCB",
|
|
48
|
+
team2: "KKR",
|
|
49
|
+
date: "2026-05-13T14:00:00Z",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 58,
|
|
53
|
+
team1: "PBKS",
|
|
54
|
+
team2: "MI",
|
|
55
|
+
date: "2026-05-14T14:00:00Z",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 59,
|
|
59
|
+
team1: "LSG",
|
|
60
|
+
team2: "CSK",
|
|
61
|
+
date: "2026-05-15T14:00:00Z",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 60,
|
|
65
|
+
team1: "KKR",
|
|
66
|
+
team2: "GT",
|
|
67
|
+
date: "2026-05-16T14:00:00Z",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 61,
|
|
71
|
+
team1: "PBKS",
|
|
72
|
+
team2: "RCB",
|
|
73
|
+
date: "2026-05-17T10:00:00Z",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 62,
|
|
77
|
+
team1: "DC",
|
|
78
|
+
team2: "RR",
|
|
79
|
+
date: "2026-05-17T14:00:00Z",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 63,
|
|
83
|
+
team1: "CSK",
|
|
84
|
+
team2: "SRH",
|
|
85
|
+
date: "2026-05-18T14:00:00Z",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 64,
|
|
89
|
+
team1: "RR",
|
|
90
|
+
team2: "LSG",
|
|
91
|
+
date: "2026-05-19T14:00:00Z",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 65,
|
|
95
|
+
team1: "KKR",
|
|
96
|
+
team2: "MI",
|
|
97
|
+
date: "2026-05-20T14:00:00Z",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 66,
|
|
101
|
+
team1: "CSK",
|
|
102
|
+
team2: "GT",
|
|
103
|
+
date: "2026-05-21T14:00:00Z",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 67,
|
|
107
|
+
team1: "SRH",
|
|
108
|
+
team2: "RCB",
|
|
109
|
+
date: "2026-05-22T14:00:00Z",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 68,
|
|
113
|
+
team1: "LSG",
|
|
114
|
+
team2: "PBKS",
|
|
115
|
+
date: "2026-05-23T14:00:00Z",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 69,
|
|
119
|
+
team1: "MI",
|
|
120
|
+
team2: "RR",
|
|
121
|
+
date: "2026-05-24T10:00:00Z",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 70,
|
|
125
|
+
team1: "KKR",
|
|
126
|
+
team2: "DC",
|
|
127
|
+
date: "2026-05-24T14:00:00Z",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
export type UpcomingMatch = Match;
|