nhl-tui 0.1.2 → 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/dist/app/input.js +18 -0
- package/dist/app/polling.js +30 -13
- package/dist/app/store.js +1 -0
- package/dist/domain/normalize.js +53 -4
- package/dist/domain/reducer.js +47 -4
- package/dist/ui/App.js +1 -1
- package/dist/ui/screens/GameDetailScreen.js +13 -8
- package/package.json +4 -1
- package/.claude/settings.local.json +0 -23
- package/.github/workflows/release.yml +0 -37
- package/CONTRIBUTING.md +0 -38
- package/src/api/nhl.ts +0 -53
- package/src/app/dates.ts +0 -54
- package/src/app/input.ts +0 -130
- package/src/app/polling.ts +0 -333
- package/src/app/store.ts +0 -55
- package/src/app/timers.ts +0 -23
- package/src/domain/diff.ts +0 -107
- package/src/domain/events.ts +0 -31
- package/src/domain/normalize.ts +0 -966
- package/src/domain/reducer.ts +0 -458
- package/src/domain/types.ts +0 -270
- package/src/index.tsx +0 -15
- package/src/ui/App.tsx +0 -151
- package/src/ui/components/Banner.tsx +0 -23
- package/src/ui/components/Footer.tsx +0 -17
- package/src/ui/components/GameList.tsx +0 -45
- package/src/ui/components/GameRow.tsx +0 -60
- package/src/ui/components/StatusLine.tsx +0 -83
- package/src/ui/screens/GameDetailScreen.tsx +0 -199
- package/src/ui/screens/LeadersScreen.tsx +0 -92
- package/src/ui/screens/ScoreboardScreen.tsx +0 -36
- package/src/ui/screens/StandingsScreen.tsx +0 -95
- package/tsconfig.json +0 -18
package/src/domain/normalize.ts
DELETED
|
@@ -1,966 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BoxScore,
|
|
3
|
-
ConferenceStandings,
|
|
4
|
-
DetailTab,
|
|
5
|
-
GameClock,
|
|
6
|
-
GameSummary,
|
|
7
|
-
GoalieStatLine,
|
|
8
|
-
LeaderEntry,
|
|
9
|
-
LeaderTable,
|
|
10
|
-
LeaderTableKey,
|
|
11
|
-
NormalizedGame,
|
|
12
|
-
NormalizedGameDetail,
|
|
13
|
-
NormalizedLeaders,
|
|
14
|
-
NormalizedPlay,
|
|
15
|
-
NormalizedStandings,
|
|
16
|
-
NormalizedTeam,
|
|
17
|
-
PlayByPlay,
|
|
18
|
-
SkaterStatLine,
|
|
19
|
-
StandingsEntry,
|
|
20
|
-
SummaryGoal,
|
|
21
|
-
SummaryPenalty,
|
|
22
|
-
TeamBoxScore,
|
|
23
|
-
ThreeStar,
|
|
24
|
-
} from "./types.js";
|
|
25
|
-
|
|
26
|
-
type RawRecord = Record<string, any>;
|
|
27
|
-
|
|
28
|
-
function readName(value: unknown): string {
|
|
29
|
-
if (typeof value === "string") {
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (value && typeof value === "object" && "default" in value) {
|
|
34
|
-
const maybeName = (value as { default?: unknown }).default;
|
|
35
|
-
if (typeof maybeName === "string") {
|
|
36
|
-
return maybeName;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return "";
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function shortName(firstName: unknown, lastName: unknown): string {
|
|
44
|
-
const first = readName(firstName);
|
|
45
|
-
const last = readName(lastName);
|
|
46
|
-
|
|
47
|
-
if (!first && !last) {
|
|
48
|
-
return "";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!first) {
|
|
52
|
-
return last;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!last) {
|
|
56
|
-
return first;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return `${first.charAt(0)}. ${last}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function formatPlayerLabel(name: string, sweaterNumber?: unknown): string {
|
|
63
|
-
if (!name) {
|
|
64
|
-
return "";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const number = toNumber(sweaterNumber);
|
|
68
|
-
return number ? `${number} ${name}` : name;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function readPlayerLabel(value: unknown, fallbackNumber?: unknown): string {
|
|
72
|
-
if (!value || typeof value !== "object") {
|
|
73
|
-
return typeof value === "string" ? formatPlayerLabel(value, fallbackNumber) : "";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const record = value as RawRecord;
|
|
77
|
-
const name = readName(record.name) || shortName(record.firstName, record.lastName);
|
|
78
|
-
|
|
79
|
-
return formatPlayerLabel(name, record.sweaterNumber ?? record.sweaterNo ?? fallbackNumber);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function toNumber(value: unknown, fallback = 0): number {
|
|
83
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (typeof value === "string" && value.trim()) {
|
|
88
|
-
const parsed = Number(value);
|
|
89
|
-
if (Number.isFinite(parsed)) {
|
|
90
|
-
return parsed;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return fallback;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function formatLocalTime(startTimeUtc: string): string {
|
|
98
|
-
const date = new Date(startTimeUtc);
|
|
99
|
-
|
|
100
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
101
|
-
hour: "numeric",
|
|
102
|
-
minute: "2-digit",
|
|
103
|
-
}).format(date);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function ordinal(value: number): string {
|
|
107
|
-
const mod100 = value % 100;
|
|
108
|
-
|
|
109
|
-
if (mod100 >= 11 && mod100 <= 13) {
|
|
110
|
-
return `${value}th`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
switch (value % 10) {
|
|
114
|
-
case 1:
|
|
115
|
-
return `${value}st`;
|
|
116
|
-
case 2:
|
|
117
|
-
return `${value}nd`;
|
|
118
|
-
case 3:
|
|
119
|
-
return `${value}rd`;
|
|
120
|
-
default:
|
|
121
|
-
return `${value}th`;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function formatPeriodLabel(
|
|
126
|
-
periodNumber?: number,
|
|
127
|
-
periodType?: string,
|
|
128
|
-
): string {
|
|
129
|
-
if (!periodNumber && !periodType) {
|
|
130
|
-
return "";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (periodType === "OT") {
|
|
134
|
-
return "OT";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (periodType === "SO") {
|
|
138
|
-
return "SO";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return ordinal(periodNumber ?? 0);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function phaseFromState(state: string): "live" | "upcoming" | "final" {
|
|
145
|
-
if (state === "LIVE" || state === "CRIT") {
|
|
146
|
-
return "live";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (state === "OFF" || state === "FINAL") {
|
|
150
|
-
return "final";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return "upcoming";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function sectionFromPhase(phase: "live" | "upcoming" | "final") {
|
|
157
|
-
switch (phase) {
|
|
158
|
-
case "live":
|
|
159
|
-
return "LIVE" as const;
|
|
160
|
-
case "final":
|
|
161
|
-
return "FINAL" as const;
|
|
162
|
-
default:
|
|
163
|
-
return "UPCOMING" as const;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function normalizeTeam(rawTeam: RawRecord | undefined): NormalizedTeam {
|
|
168
|
-
const name = rawTeam?.name ?? rawTeam?.commonName;
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
id: toNumber(rawTeam?.id),
|
|
172
|
-
abbrev: readName(rawTeam?.abbrev) || "---",
|
|
173
|
-
location: readName(rawTeam?.placeName),
|
|
174
|
-
shortName: readName(name),
|
|
175
|
-
score: toNumber(rawTeam?.score),
|
|
176
|
-
shotsOnGoal: rawTeam?.sog === undefined ? undefined : toNumber(rawTeam.sog),
|
|
177
|
-
record:
|
|
178
|
-
typeof rawTeam?.record === "string" && rawTeam.record.trim()
|
|
179
|
-
? rawTeam.record
|
|
180
|
-
: undefined,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function normalizeClock(
|
|
185
|
-
rawClock: RawRecord | undefined,
|
|
186
|
-
rawPeriod: RawRecord | undefined,
|
|
187
|
-
): GameClock | undefined {
|
|
188
|
-
if (!rawClock || !rawPeriod) {
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const periodNumber = toNumber(rawPeriod.number);
|
|
193
|
-
const periodType =
|
|
194
|
-
typeof rawPeriod.periodType === "string" ? rawPeriod.periodType : "REG";
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
periodNumber,
|
|
198
|
-
periodType,
|
|
199
|
-
periodLabel: formatPeriodLabel(periodNumber, periodType),
|
|
200
|
-
timeRemaining:
|
|
201
|
-
typeof rawClock.timeRemaining === "string"
|
|
202
|
-
? rawClock.timeRemaining
|
|
203
|
-
: "--:--",
|
|
204
|
-
running: Boolean(rawClock.running),
|
|
205
|
-
inIntermission: Boolean(rawClock.inIntermission),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function finalContext(clock?: GameClock): string {
|
|
210
|
-
if (!clock) {
|
|
211
|
-
return "Final";
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (clock.periodType === "OT") {
|
|
215
|
-
return "Final/OT";
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (clock.periodType === "SO") {
|
|
219
|
-
return "Final/SO";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return "Final";
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function liveContext(clock?: GameClock): string {
|
|
226
|
-
if (!clock) {
|
|
227
|
-
return "Live";
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (clock.inIntermission) {
|
|
231
|
-
return `${clock.periodLabel} INT`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return `${clock.periodLabel} ${clock.timeRemaining}`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function normalizeGame(rawGame: unknown): NormalizedGame {
|
|
238
|
-
const game = rawGame as RawRecord;
|
|
239
|
-
const away = normalizeTeam(game.awayTeam);
|
|
240
|
-
const home = normalizeTeam(game.homeTeam);
|
|
241
|
-
const state = typeof game.gameState === "string" ? game.gameState : "FUT";
|
|
242
|
-
const phase = phaseFromState(state);
|
|
243
|
-
const clock = normalizeClock(game.clock, game.periodDescriptor);
|
|
244
|
-
const startTimeUtc =
|
|
245
|
-
typeof game.startTimeUTC === "string" ? game.startTimeUTC : "";
|
|
246
|
-
const startTimeLabel = startTimeUtc ? formatLocalTime(startTimeUtc) : "--:--";
|
|
247
|
-
|
|
248
|
-
let statusLabel = "UPCOMING";
|
|
249
|
-
let contextLabel = `Puck Drop ${startTimeLabel}`;
|
|
250
|
-
|
|
251
|
-
if (phase === "live") {
|
|
252
|
-
statusLabel = "LIVE";
|
|
253
|
-
contextLabel = liveContext(clock);
|
|
254
|
-
} else if (phase === "final") {
|
|
255
|
-
statusLabel = "FINAL";
|
|
256
|
-
contextLabel = finalContext(clock);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
id: toNumber(game.id),
|
|
261
|
-
season: toNumber(game.season),
|
|
262
|
-
state,
|
|
263
|
-
phase,
|
|
264
|
-
section: sectionFromPhase(phase),
|
|
265
|
-
startTimeUtc,
|
|
266
|
-
startTimeEpochMs: Number.isNaN(Date.parse(startTimeUtc))
|
|
267
|
-
? 0
|
|
268
|
-
: Date.parse(startTimeUtc),
|
|
269
|
-
startTimeLabel,
|
|
270
|
-
statusLabel,
|
|
271
|
-
contextLabel,
|
|
272
|
-
periodLabel: clock?.periodLabel,
|
|
273
|
-
venue: readName(game.venue) || readName(game.venueLocation) || "",
|
|
274
|
-
away,
|
|
275
|
-
home,
|
|
276
|
-
clock,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function sortGames(left: NormalizedGame, right: NormalizedGame): number {
|
|
281
|
-
const sectionOrder = {
|
|
282
|
-
LIVE: 0,
|
|
283
|
-
UPCOMING: 1,
|
|
284
|
-
FINAL: 2,
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
const sectionDelta =
|
|
288
|
-
sectionOrder[left.section] - sectionOrder[right.section];
|
|
289
|
-
if (sectionDelta !== 0) {
|
|
290
|
-
return sectionDelta;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return left.startTimeEpochMs - right.startTimeEpochMs || left.id - right.id;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function normalizeScoreboard(rawPayload: unknown): NormalizedGame[] {
|
|
297
|
-
const payload = rawPayload as RawRecord;
|
|
298
|
-
const games = Array.isArray(payload.games) ? payload.games : [];
|
|
299
|
-
|
|
300
|
-
return games.map(normalizeGame).sort(sortGames);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function normalizeGoal(
|
|
304
|
-
rawGoal: RawRecord,
|
|
305
|
-
periodLabel: string,
|
|
306
|
-
playerNumbers?: Map<number, number>,
|
|
307
|
-
): SummaryGoal {
|
|
308
|
-
const assists = Array.isArray(rawGoal.assists)
|
|
309
|
-
? rawGoal.assists
|
|
310
|
-
.map((assist) => {
|
|
311
|
-
const assistRecord = assist as RawRecord;
|
|
312
|
-
return readPlayerLabel(
|
|
313
|
-
assistRecord,
|
|
314
|
-
assistRecord.sweaterNumber ?? playerNumbers?.get(toNumber(assistRecord.playerId)),
|
|
315
|
-
);
|
|
316
|
-
})
|
|
317
|
-
.filter(Boolean)
|
|
318
|
-
: [];
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
eventId: toNumber(rawGoal.eventId),
|
|
322
|
-
team: readName(rawGoal.teamAbbrev),
|
|
323
|
-
scorer: formatPlayerLabel(
|
|
324
|
-
readName(rawGoal.name),
|
|
325
|
-
playerNumbers?.get(toNumber(rawGoal.playerId)),
|
|
326
|
-
),
|
|
327
|
-
strength:
|
|
328
|
-
typeof rawGoal.strength === "string"
|
|
329
|
-
? rawGoal.strength.toUpperCase()
|
|
330
|
-
: "EV",
|
|
331
|
-
timeInPeriod:
|
|
332
|
-
typeof rawGoal.timeInPeriod === "string" ? rawGoal.timeInPeriod : "--:--",
|
|
333
|
-
periodLabel,
|
|
334
|
-
scoreAfter: `${toNumber(rawGoal.awayScore)}-${toNumber(rawGoal.homeScore)}`,
|
|
335
|
-
assists,
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function normalizePenalty(
|
|
340
|
-
rawPenalty: RawRecord,
|
|
341
|
-
periodLabel: string,
|
|
342
|
-
): SummaryPenalty {
|
|
343
|
-
return {
|
|
344
|
-
team: readName(rawPenalty.teamAbbrev),
|
|
345
|
-
player: readPlayerLabel(rawPenalty.committedByPlayer),
|
|
346
|
-
drawnBy: readPlayerLabel(rawPenalty.drawnBy) || undefined,
|
|
347
|
-
kind:
|
|
348
|
-
typeof rawPenalty.descKey === "string"
|
|
349
|
-
? titleCase(rawPenalty.descKey)
|
|
350
|
-
: "",
|
|
351
|
-
duration: toNumber(rawPenalty.duration),
|
|
352
|
-
timeInPeriod:
|
|
353
|
-
typeof rawPenalty.timeInPeriod === "string"
|
|
354
|
-
? rawPenalty.timeInPeriod
|
|
355
|
-
: "--:--",
|
|
356
|
-
periodLabel,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function normalizeThreeStar(
|
|
361
|
-
rawStar: RawRecord,
|
|
362
|
-
playerNumbers?: Map<number, number>,
|
|
363
|
-
): ThreeStar {
|
|
364
|
-
const savePct = toNumber(rawStar.savePctg);
|
|
365
|
-
const gaa = toNumber(rawStar.goalsAgainstAverage);
|
|
366
|
-
const goalieStat = savePct ? `SV% ${savePct.toFixed(3)}` : "";
|
|
367
|
-
const skaterStat = gaa ? `GAA ${gaa.toFixed(2)}` : "";
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
star: toNumber(rawStar.star),
|
|
371
|
-
player: formatPlayerLabel(
|
|
372
|
-
readName(rawStar.name),
|
|
373
|
-
rawStar.sweaterNo ?? playerNumbers?.get(toNumber(rawStar.playerId)),
|
|
374
|
-
),
|
|
375
|
-
team: typeof rawStar.teamAbbrev === "string" ? rawStar.teamAbbrev : "",
|
|
376
|
-
position: typeof rawStar.position === "string" ? rawStar.position : "",
|
|
377
|
-
statLine: goalieStat || skaterStat,
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function titleCase(value: string): string {
|
|
382
|
-
return value
|
|
383
|
-
.split(/[\s-]+/)
|
|
384
|
-
.filter(Boolean)
|
|
385
|
-
.map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1).toLowerCase())
|
|
386
|
-
.join(" ");
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function buildPlayerNumberMap(rawBoxPayload: RawRecord | undefined): Map<number, number> {
|
|
390
|
-
const playerNumbers = new Map<number, number>();
|
|
391
|
-
const playerByGameStats = (rawBoxPayload?.playerByGameStats ?? {}) as RawRecord;
|
|
392
|
-
|
|
393
|
-
for (const side of ["awayTeam", "homeTeam"] as const) {
|
|
394
|
-
const teamStats = (playerByGameStats[side] ?? {}) as RawRecord;
|
|
395
|
-
|
|
396
|
-
for (const section of ["forwards", "defense", "goalies"] as const) {
|
|
397
|
-
const players = Array.isArray(teamStats[section]) ? teamStats[section] : [];
|
|
398
|
-
|
|
399
|
-
for (const player of players) {
|
|
400
|
-
const record = player as RawRecord;
|
|
401
|
-
const playerId = toNumber(record.playerId);
|
|
402
|
-
const sweaterNumber = toNumber(record.sweaterNumber);
|
|
403
|
-
|
|
404
|
-
if (playerId && sweaterNumber) {
|
|
405
|
-
playerNumbers.set(playerId, sweaterNumber);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return playerNumbers;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function normalizeSummary(
|
|
415
|
-
rawSummary: RawRecord | undefined,
|
|
416
|
-
playerNumbers?: Map<number, number>,
|
|
417
|
-
): GameSummary {
|
|
418
|
-
const scoring = Array.isArray(rawSummary?.scoring) ? rawSummary.scoring : [];
|
|
419
|
-
const penalties = Array.isArray(rawSummary?.penalties)
|
|
420
|
-
? rawSummary.penalties
|
|
421
|
-
: [];
|
|
422
|
-
const threeStars = Array.isArray(rawSummary?.threeStars)
|
|
423
|
-
? rawSummary.threeStars
|
|
424
|
-
: [];
|
|
425
|
-
|
|
426
|
-
return {
|
|
427
|
-
scoring: scoring.map((period) => {
|
|
428
|
-
const periodRecord = period as RawRecord;
|
|
429
|
-
const descriptor = periodRecord.periodDescriptor as RawRecord | undefined;
|
|
430
|
-
const periodLabel = formatPeriodLabel(
|
|
431
|
-
toNumber(descriptor?.number),
|
|
432
|
-
typeof descriptor?.periodType === "string"
|
|
433
|
-
? descriptor.periodType
|
|
434
|
-
: undefined,
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
periodLabel,
|
|
439
|
-
goals: Array.isArray(periodRecord.goals)
|
|
440
|
-
? periodRecord.goals.map((goal) =>
|
|
441
|
-
normalizeGoal(goal as RawRecord, periodLabel, playerNumbers),
|
|
442
|
-
)
|
|
443
|
-
: [],
|
|
444
|
-
};
|
|
445
|
-
}),
|
|
446
|
-
penalties: penalties.map((period) => {
|
|
447
|
-
const periodRecord = period as RawRecord;
|
|
448
|
-
const descriptor = periodRecord.periodDescriptor as RawRecord | undefined;
|
|
449
|
-
const periodLabel = formatPeriodLabel(
|
|
450
|
-
toNumber(descriptor?.number),
|
|
451
|
-
typeof descriptor?.periodType === "string"
|
|
452
|
-
? descriptor.periodType
|
|
453
|
-
: undefined,
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
periodLabel,
|
|
458
|
-
penalties: Array.isArray(periodRecord.penalties)
|
|
459
|
-
? periodRecord.penalties.map((penalty) =>
|
|
460
|
-
normalizePenalty(penalty as RawRecord, periodLabel),
|
|
461
|
-
)
|
|
462
|
-
: [],
|
|
463
|
-
};
|
|
464
|
-
}),
|
|
465
|
-
threeStars: threeStars.map((star) =>
|
|
466
|
-
normalizeThreeStar(star as RawRecord, playerNumbers),
|
|
467
|
-
),
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function prettyPlayType(type: string): string {
|
|
472
|
-
return type
|
|
473
|
-
.split("-")
|
|
474
|
-
.map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1))
|
|
475
|
-
.join(" ");
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function playerName(playerById: Map<number, string>, playerId: unknown): string {
|
|
479
|
-
const id = toNumber(playerId);
|
|
480
|
-
|
|
481
|
-
return id ? playerById.get(id) ?? `#${id}` : "";
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function normalizePlay(
|
|
485
|
-
rawPlay: RawRecord,
|
|
486
|
-
playerById: Map<number, string>,
|
|
487
|
-
teamById: Map<number, string>,
|
|
488
|
-
): NormalizedPlay {
|
|
489
|
-
const type = typeof rawPlay.typeDescKey === "string" ? rawPlay.typeDescKey : "";
|
|
490
|
-
const details = (rawPlay.details ?? {}) as RawRecord;
|
|
491
|
-
const periodDescriptor = rawPlay.periodDescriptor as RawRecord | undefined;
|
|
492
|
-
const periodLabel = formatPeriodLabel(
|
|
493
|
-
toNumber(periodDescriptor?.number),
|
|
494
|
-
typeof periodDescriptor?.periodType === "string"
|
|
495
|
-
? periodDescriptor.periodType
|
|
496
|
-
: undefined,
|
|
497
|
-
);
|
|
498
|
-
const team = teamById.get(toNumber(details.eventOwnerTeamId));
|
|
499
|
-
let title = prettyPlayType(type || "play");
|
|
500
|
-
let detail = "";
|
|
501
|
-
|
|
502
|
-
if (type === "goal") {
|
|
503
|
-
title = "Goal";
|
|
504
|
-
const scorer = playerName(playerById, details.scoringPlayerId);
|
|
505
|
-
const assists = [details.assist1PlayerId, details.assist2PlayerId]
|
|
506
|
-
.map((playerId) => playerName(playerById, playerId))
|
|
507
|
-
.filter(Boolean);
|
|
508
|
-
const shotType =
|
|
509
|
-
typeof details.shotType === "string" ? titleCase(details.shotType) : "";
|
|
510
|
-
|
|
511
|
-
detail = [scorer, shotType ? `${shotType} shot` : "", assists.length ? `A: ${assists.join(", ")}` : ""]
|
|
512
|
-
.filter(Boolean)
|
|
513
|
-
.join(" ");
|
|
514
|
-
} else if (type === "shot-on-goal") {
|
|
515
|
-
title = "Shot";
|
|
516
|
-
const shooter = playerName(playerById, details.shootingPlayerId);
|
|
517
|
-
const shotType =
|
|
518
|
-
typeof details.shotType === "string" ? `${titleCase(details.shotType)} shot` : "";
|
|
519
|
-
detail = [shooter, shotType].filter(Boolean).join(" ");
|
|
520
|
-
} else if (type === "penalty") {
|
|
521
|
-
title = "Penalty";
|
|
522
|
-
const offender = playerName(playerById, details.committedByPlayerId);
|
|
523
|
-
const reason =
|
|
524
|
-
typeof details.descKey === "string" ? titleCase(details.descKey) : "";
|
|
525
|
-
detail = [offender, reason].filter(Boolean).join(" ");
|
|
526
|
-
} else if (type === "faceoff") {
|
|
527
|
-
title = "Faceoff";
|
|
528
|
-
const winner = playerName(playerById, details.winningPlayerId);
|
|
529
|
-
const loser = playerName(playerById, details.losingPlayerId);
|
|
530
|
-
detail = [winner, loser ? `beat ${loser}` : ""].filter(Boolean).join(" ");
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const awayScore = details.awayScore;
|
|
534
|
-
const homeScore = details.homeScore;
|
|
535
|
-
|
|
536
|
-
return {
|
|
537
|
-
id: toNumber(rawPlay.eventId),
|
|
538
|
-
sortOrder: toNumber(rawPlay.sortOrder),
|
|
539
|
-
type,
|
|
540
|
-
team:
|
|
541
|
-
typeof details.teamAbbrev === "string"
|
|
542
|
-
? details.teamAbbrev
|
|
543
|
-
: typeof details.eventOwnerTeamAbbrev === "string"
|
|
544
|
-
? details.eventOwnerTeamAbbrev
|
|
545
|
-
: team,
|
|
546
|
-
title,
|
|
547
|
-
detail: detail || undefined,
|
|
548
|
-
score:
|
|
549
|
-
awayScore !== undefined && homeScore !== undefined
|
|
550
|
-
? `${toNumber(awayScore)}-${toNumber(homeScore)}`
|
|
551
|
-
: undefined,
|
|
552
|
-
isGoal: type === "goal",
|
|
553
|
-
periodLabel,
|
|
554
|
-
timeInPeriod:
|
|
555
|
-
typeof rawPlay.timeInPeriod === "string" ? rawPlay.timeInPeriod : "--:--",
|
|
556
|
-
timeRemaining:
|
|
557
|
-
typeof rawPlay.timeRemaining === "string"
|
|
558
|
-
? rawPlay.timeRemaining
|
|
559
|
-
: "--:--",
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function normalizePlayByPlay(rawPayload: RawRecord): PlayByPlay {
|
|
564
|
-
const plays = Array.isArray(rawPayload.plays) ? rawPayload.plays : [];
|
|
565
|
-
const rosterSpots = Array.isArray(rawPayload.rosterSpots)
|
|
566
|
-
? rawPayload.rosterSpots
|
|
567
|
-
: [];
|
|
568
|
-
const playerById = new Map<number, string>();
|
|
569
|
-
|
|
570
|
-
for (const spot of rosterSpots) {
|
|
571
|
-
const player = spot as RawRecord;
|
|
572
|
-
const playerId = toNumber(player.playerId);
|
|
573
|
-
if (!playerId) {
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const firstName = readName(player.firstName);
|
|
578
|
-
const lastName = readName(player.lastName);
|
|
579
|
-
const displayName = [firstName, lastName].filter(Boolean).join(" ");
|
|
580
|
-
playerById.set(playerId, displayName || `#${playerId}`);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const teamById = new Map<number, string>();
|
|
584
|
-
const awayTeam = normalizeTeam(rawPayload.awayTeam as RawRecord | undefined);
|
|
585
|
-
const homeTeam = normalizeTeam(rawPayload.homeTeam as RawRecord | undefined);
|
|
586
|
-
teamById.set(awayTeam.id, awayTeam.abbrev);
|
|
587
|
-
teamById.set(homeTeam.id, homeTeam.abbrev);
|
|
588
|
-
|
|
589
|
-
const normalized = plays.map((play) =>
|
|
590
|
-
normalizePlay(play as RawRecord, playerById, teamById),
|
|
591
|
-
);
|
|
592
|
-
|
|
593
|
-
return {
|
|
594
|
-
plays: normalized,
|
|
595
|
-
lastEventId: normalized.length ? normalized[normalized.length - 1].id : undefined,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function parseSkaters(rawTeam: RawRecord | undefined): SkaterStatLine[] {
|
|
600
|
-
const forwards = Array.isArray(rawTeam?.forwards) ? rawTeam.forwards : [];
|
|
601
|
-
const defense = Array.isArray(rawTeam?.defense) ? rawTeam.defense : [];
|
|
602
|
-
const skaters = [...forwards, ...defense];
|
|
603
|
-
|
|
604
|
-
return skaters
|
|
605
|
-
.map((player) => {
|
|
606
|
-
const skater = player as RawRecord;
|
|
607
|
-
|
|
608
|
-
return {
|
|
609
|
-
playerId: toNumber(skater.playerId),
|
|
610
|
-
sweaterNumber:
|
|
611
|
-
skater.sweaterNumber === undefined
|
|
612
|
-
? undefined
|
|
613
|
-
: toNumber(skater.sweaterNumber),
|
|
614
|
-
name: readName(skater.name),
|
|
615
|
-
position: typeof skater.position === "string" ? skater.position : "",
|
|
616
|
-
goals: toNumber(skater.goals),
|
|
617
|
-
assists: toNumber(skater.assists),
|
|
618
|
-
points: toNumber(skater.points),
|
|
619
|
-
plusMinus:
|
|
620
|
-
skater.plusMinus === undefined ? undefined : toNumber(skater.plusMinus),
|
|
621
|
-
shots: toNumber(skater.sog),
|
|
622
|
-
hits: toNumber(skater.hits),
|
|
623
|
-
pim: toNumber(skater.pim),
|
|
624
|
-
toi: typeof skater.toi === "string" ? skater.toi : "00:00",
|
|
625
|
-
};
|
|
626
|
-
})
|
|
627
|
-
.sort((left, right) => {
|
|
628
|
-
return (
|
|
629
|
-
right.points - left.points ||
|
|
630
|
-
right.goals - left.goals ||
|
|
631
|
-
right.shots - left.shots ||
|
|
632
|
-
right.hits - left.hits
|
|
633
|
-
);
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function parseGoalies(rawTeam: RawRecord | undefined): GoalieStatLine[] {
|
|
638
|
-
const goalies = Array.isArray(rawTeam?.goalies) ? rawTeam.goalies : [];
|
|
639
|
-
|
|
640
|
-
return goalies.map((player) => {
|
|
641
|
-
const goalie = player as RawRecord;
|
|
642
|
-
const shotsAgainst = toNumber(goalie.shotsAgainst);
|
|
643
|
-
const saves = toNumber(goalie.saves);
|
|
644
|
-
|
|
645
|
-
return {
|
|
646
|
-
playerId: toNumber(goalie.playerId),
|
|
647
|
-
sweaterNumber:
|
|
648
|
-
goalie.sweaterNumber === undefined
|
|
649
|
-
? undefined
|
|
650
|
-
: toNumber(goalie.sweaterNumber),
|
|
651
|
-
name: readName(goalie.name),
|
|
652
|
-
saves,
|
|
653
|
-
shotsAgainst,
|
|
654
|
-
goalsAgainst: toNumber(goalie.goalsAgainst),
|
|
655
|
-
savePct: shotsAgainst ? saves / shotsAgainst : 0,
|
|
656
|
-
toi: typeof goalie.toi === "string" ? goalie.toi : "00:00",
|
|
657
|
-
};
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function normalizeTeamBoxScore(
|
|
662
|
-
rawGame: RawRecord,
|
|
663
|
-
rawTeamStats: RawRecord | undefined,
|
|
664
|
-
side: "away" | "home",
|
|
665
|
-
): TeamBoxScore {
|
|
666
|
-
const team = normalizeTeam(
|
|
667
|
-
side === "away" ? rawGame.awayTeam : rawGame.homeTeam,
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
return {
|
|
671
|
-
team,
|
|
672
|
-
skaters: parseSkaters(rawTeamStats),
|
|
673
|
-
goalies: parseGoalies(rawTeamStats),
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function normalizeBoxScore(rawPayload: RawRecord): BoxScore {
|
|
678
|
-
const playerByGameStats = (rawPayload.playerByGameStats ?? {}) as RawRecord;
|
|
679
|
-
|
|
680
|
-
return {
|
|
681
|
-
away: normalizeTeamBoxScore(
|
|
682
|
-
rawPayload,
|
|
683
|
-
playerByGameStats.awayTeam as RawRecord | undefined,
|
|
684
|
-
"away",
|
|
685
|
-
),
|
|
686
|
-
home: normalizeTeamBoxScore(
|
|
687
|
-
rawPayload,
|
|
688
|
-
playerByGameStats.homeTeam as RawRecord | undefined,
|
|
689
|
-
"home",
|
|
690
|
-
),
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const DIVISION_NAME_BY_ABBREV: Record<string, string> = {
|
|
695
|
-
A: "Atlantic",
|
|
696
|
-
M: "Metropolitan",
|
|
697
|
-
C: "Central",
|
|
698
|
-
P: "Pacific",
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
const CONFERENCE_NAME_BY_ABBREV: Record<string, string> = {
|
|
702
|
-
E: "Eastern",
|
|
703
|
-
W: "Western",
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
function fallbackDivisionName(divisionAbbrev: string): string {
|
|
707
|
-
return DIVISION_NAME_BY_ABBREV[divisionAbbrev] ?? divisionAbbrev;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function fallbackConferenceName(conferenceAbbrev: string): string {
|
|
711
|
-
return CONFERENCE_NAME_BY_ABBREV[conferenceAbbrev] ?? conferenceAbbrev;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function normalizeStandingsEntry(rawStanding: RawRecord): StandingsEntry {
|
|
715
|
-
const conferenceAbbrev =
|
|
716
|
-
typeof rawStanding.conferenceAbbrev === "string"
|
|
717
|
-
? rawStanding.conferenceAbbrev
|
|
718
|
-
: "";
|
|
719
|
-
const divisionAbbrev =
|
|
720
|
-
typeof rawStanding.divisionAbbrev === "string"
|
|
721
|
-
? rawStanding.divisionAbbrev
|
|
722
|
-
: "";
|
|
723
|
-
const streakCode =
|
|
724
|
-
typeof rawStanding.streakCode === "string" ? rawStanding.streakCode : "";
|
|
725
|
-
const streakCount = toNumber(rawStanding.streakCount);
|
|
726
|
-
|
|
727
|
-
return {
|
|
728
|
-
teamAbbrev: readName(rawStanding.teamAbbrev) || "---",
|
|
729
|
-
teamName:
|
|
730
|
-
readName(rawStanding.teamCommonName) || readName(rawStanding.teamName),
|
|
731
|
-
conferenceAbbrev,
|
|
732
|
-
conferenceName:
|
|
733
|
-
readName(rawStanding.conferenceName) ||
|
|
734
|
-
fallbackConferenceName(conferenceAbbrev),
|
|
735
|
-
divisionAbbrev,
|
|
736
|
-
divisionName:
|
|
737
|
-
readName(rawStanding.divisionName) || fallbackDivisionName(divisionAbbrev),
|
|
738
|
-
divisionRank: toNumber(rawStanding.divisionSequence),
|
|
739
|
-
conferenceRank: toNumber(rawStanding.conferenceSequence),
|
|
740
|
-
wildcardRank: toNumber(rawStanding.wildcardSequence),
|
|
741
|
-
leagueRank: toNumber(rawStanding.leagueSequence),
|
|
742
|
-
points: toNumber(rawStanding.points),
|
|
743
|
-
gamesPlayed: toNumber(rawStanding.gamesPlayed),
|
|
744
|
-
wins: toNumber(rawStanding.wins),
|
|
745
|
-
losses: toNumber(rawStanding.losses),
|
|
746
|
-
otLosses: toNumber(rawStanding.otLosses),
|
|
747
|
-
row: toNumber(rawStanding.regulationPlusOtWins),
|
|
748
|
-
streak: streakCode && streakCount ? streakCode + String(streakCount) : "-",
|
|
749
|
-
clinchIndicator:
|
|
750
|
-
typeof rawStanding.clinchIndicator === "string" &&
|
|
751
|
-
rawStanding.clinchIndicator.trim()
|
|
752
|
-
? rawStanding.clinchIndicator
|
|
753
|
-
: undefined,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function buildConferenceStandings(
|
|
758
|
-
entries: StandingsEntry[],
|
|
759
|
-
conferenceAbbrev: string,
|
|
760
|
-
): ConferenceStandings {
|
|
761
|
-
const conferenceEntries = entries.filter(
|
|
762
|
-
(entry) => entry.conferenceAbbrev === conferenceAbbrev,
|
|
763
|
-
);
|
|
764
|
-
const divisionOrder = conferenceAbbrev === "E" ? ["A", "M"] : ["C", "P"];
|
|
765
|
-
const sections = [];
|
|
766
|
-
|
|
767
|
-
for (const divisionAbbrev of divisionOrder) {
|
|
768
|
-
const divisionEntries = conferenceEntries
|
|
769
|
-
.filter(
|
|
770
|
-
(entry) =>
|
|
771
|
-
entry.divisionAbbrev === divisionAbbrev &&
|
|
772
|
-
entry.divisionRank > 0 &&
|
|
773
|
-
entry.divisionRank <= 3,
|
|
774
|
-
)
|
|
775
|
-
.sort((left, right) => {
|
|
776
|
-
return (
|
|
777
|
-
left.divisionRank - right.divisionRank ||
|
|
778
|
-
right.points - left.points ||
|
|
779
|
-
left.teamAbbrev.localeCompare(right.teamAbbrev)
|
|
780
|
-
);
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
sections.push({
|
|
784
|
-
title: (divisionEntries[0]?.divisionName || fallbackDivisionName(divisionAbbrev)).toUpperCase(),
|
|
785
|
-
entries: divisionEntries,
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
sections.push({
|
|
790
|
-
title: "WILD CARD",
|
|
791
|
-
entries: conferenceEntries
|
|
792
|
-
.filter((entry) => entry.wildcardRank > 0 && entry.wildcardRank <= 2)
|
|
793
|
-
.sort((left, right) => {
|
|
794
|
-
return (
|
|
795
|
-
left.wildcardRank - right.wildcardRank ||
|
|
796
|
-
right.points - left.points ||
|
|
797
|
-
left.teamAbbrev.localeCompare(right.teamAbbrev)
|
|
798
|
-
);
|
|
799
|
-
}),
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
sections.push({
|
|
803
|
-
title: "UNDER WILD CARD",
|
|
804
|
-
entries: conferenceEntries
|
|
805
|
-
.filter((entry) => entry.wildcardRank > 2)
|
|
806
|
-
.sort((left, right) => {
|
|
807
|
-
return (
|
|
808
|
-
left.wildcardRank - right.wildcardRank ||
|
|
809
|
-
right.points - left.points ||
|
|
810
|
-
left.teamAbbrev.localeCompare(right.teamAbbrev)
|
|
811
|
-
);
|
|
812
|
-
}),
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
return {
|
|
816
|
-
conferenceAbbrev,
|
|
817
|
-
conferenceName:
|
|
818
|
-
conferenceEntries[0]?.conferenceName ||
|
|
819
|
-
fallbackConferenceName(conferenceAbbrev),
|
|
820
|
-
sections,
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
export function normalizeStandings(
|
|
825
|
-
rawPayload: unknown,
|
|
826
|
-
scoreboardDate: string,
|
|
827
|
-
now = Date.now(),
|
|
828
|
-
): NormalizedStandings {
|
|
829
|
-
const payload = rawPayload as RawRecord;
|
|
830
|
-
const standings = Array.isArray(payload.standings) ? payload.standings : [];
|
|
831
|
-
const entries = standings.map((standing) =>
|
|
832
|
-
normalizeStandingsEntry(standing as RawRecord),
|
|
833
|
-
);
|
|
834
|
-
|
|
835
|
-
return {
|
|
836
|
-
date: scoreboardDate,
|
|
837
|
-
conferences: ["E", "W"].map((conferenceAbbrev) =>
|
|
838
|
-
buildConferenceStandings(entries, conferenceAbbrev),
|
|
839
|
-
),
|
|
840
|
-
lastUpdatedAt: now,
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
const SKATER_LEADER_META = [
|
|
845
|
-
{ key: "points", title: "Points", valueLabel: "PTS", digits: 0 },
|
|
846
|
-
{ key: "goals", title: "Goals", valueLabel: "G", digits: 0 },
|
|
847
|
-
{ key: "assists", title: "Assists", valueLabel: "A", digits: 0 },
|
|
848
|
-
] as const;
|
|
849
|
-
|
|
850
|
-
const GOALIE_LEADER_META = [
|
|
851
|
-
{
|
|
852
|
-
key: "goalsAgainstAverage",
|
|
853
|
-
title: "Goals Against Average",
|
|
854
|
-
valueLabel: "GAA",
|
|
855
|
-
digits: 2,
|
|
856
|
-
},
|
|
857
|
-
{ key: "savePctg", title: "Save Percentage", valueLabel: "SV%", digits: 3 },
|
|
858
|
-
{ key: "shutouts", title: "Shutouts", valueLabel: "SO", digits: 0 },
|
|
859
|
-
] as const;
|
|
860
|
-
|
|
861
|
-
function formatLeaderValue(value: number, digits: number): string {
|
|
862
|
-
return digits === 0 ? String(value) : value.toFixed(digits);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function normalizeLeaderEntry(
|
|
866
|
-
rawLeader: RawRecord,
|
|
867
|
-
rank: number,
|
|
868
|
-
digits: number,
|
|
869
|
-
): LeaderEntry {
|
|
870
|
-
const value = toNumber(rawLeader.value);
|
|
871
|
-
|
|
872
|
-
return {
|
|
873
|
-
rank,
|
|
874
|
-
playerId: toNumber(rawLeader.id),
|
|
875
|
-
player: formatPlayerLabel(
|
|
876
|
-
shortName(rawLeader.firstName, rawLeader.lastName),
|
|
877
|
-
rawLeader.sweaterNumber,
|
|
878
|
-
),
|
|
879
|
-
teamAbbrev:
|
|
880
|
-
typeof rawLeader.teamAbbrev === "string"
|
|
881
|
-
? rawLeader.teamAbbrev
|
|
882
|
-
: readName(rawLeader.teamAbbrev) || "---",
|
|
883
|
-
position: typeof rawLeader.position === "string" ? rawLeader.position : "",
|
|
884
|
-
value,
|
|
885
|
-
displayValue: formatLeaderValue(value, digits),
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function normalizeLeaderTable(
|
|
890
|
-
rawPayload: RawRecord,
|
|
891
|
-
meta: {
|
|
892
|
-
key: LeaderTableKey;
|
|
893
|
-
title: string;
|
|
894
|
-
valueLabel: string;
|
|
895
|
-
digits: number;
|
|
896
|
-
},
|
|
897
|
-
): LeaderTable {
|
|
898
|
-
const entries = (Array.isArray(rawPayload[meta.key]) ? rawPayload[meta.key] : []) as RawRecord[];
|
|
899
|
-
|
|
900
|
-
return {
|
|
901
|
-
key: meta.key,
|
|
902
|
-
title: meta.title,
|
|
903
|
-
valueLabel: meta.valueLabel,
|
|
904
|
-
entries: entries
|
|
905
|
-
.slice(0, 10)
|
|
906
|
-
.map((entry: RawRecord, index: number) =>
|
|
907
|
-
normalizeLeaderEntry(entry, index + 1, meta.digits),
|
|
908
|
-
),
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
export function normalizeLeaders(
|
|
913
|
-
rawSkaterPayload: unknown,
|
|
914
|
-
rawGoaliePayload: unknown,
|
|
915
|
-
now = Date.now(),
|
|
916
|
-
): NormalizedLeaders {
|
|
917
|
-
const skaterPayload = rawSkaterPayload as RawRecord;
|
|
918
|
-
const goaliePayload = rawGoaliePayload as RawRecord;
|
|
919
|
-
|
|
920
|
-
return {
|
|
921
|
-
skaterTables: SKATER_LEADER_META.map((meta) =>
|
|
922
|
-
normalizeLeaderTable(skaterPayload, meta),
|
|
923
|
-
),
|
|
924
|
-
goalieTables: GOALIE_LEADER_META.map((meta) =>
|
|
925
|
-
normalizeLeaderTable(goaliePayload, meta),
|
|
926
|
-
),
|
|
927
|
-
lastUpdatedAt: now,
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
export function normalizeDetail(
|
|
932
|
-
tab: DetailTab,
|
|
933
|
-
rawPayload: unknown,
|
|
934
|
-
now = Date.now(),
|
|
935
|
-
): NormalizedGameDetail {
|
|
936
|
-
const payload = rawPayload as RawRecord;
|
|
937
|
-
const summaryPayload =
|
|
938
|
-
tab === "summary" && payload.landing ? (payload.landing as RawRecord) : payload;
|
|
939
|
-
const boxPayload =
|
|
940
|
-
tab === "summary" && payload.box ? (payload.box as RawRecord) : undefined;
|
|
941
|
-
const detail: NormalizedGameDetail = {
|
|
942
|
-
game: normalizeGame(summaryPayload),
|
|
943
|
-
lastUpdatedAt: now,
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
if (tab === "summary") {
|
|
947
|
-
const playerNumbers = buildPlayerNumberMap(boxPayload);
|
|
948
|
-
detail.summary = normalizeSummary(
|
|
949
|
-
summaryPayload.summary as RawRecord | undefined,
|
|
950
|
-
playerNumbers,
|
|
951
|
-
);
|
|
952
|
-
if (boxPayload) {
|
|
953
|
-
detail.box = normalizeBoxScore(boxPayload);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
if (tab === "pbp") {
|
|
958
|
-
detail.pbp = normalizePlayByPlay(payload);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (tab === "box") {
|
|
962
|
-
detail.box = normalizeBoxScore(payload);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
return detail;
|
|
966
|
-
}
|