Skip to content

Commit 5f620fc

Browse files
committed
Refactor
1 parent 6961fa1 commit 5f620fc

File tree

4 files changed

+356
-323
lines changed

4 files changed

+356
-323
lines changed

TODO

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
season range highlighting like sports-reference on click highlight
22
- could compute stats in worker with seasonRange
3+
- first click - highlight every row for that season
4+
- second click - highlight every row for that season, and seasons in between
5+
- should work regardless of sorting - still pick by season
6+
- need to control table row highlight state outside of the table
7+
- use normal row click feature, or build own and just pass a class down to each row? the latter might be easier
8+
- mobile UI
9+
- when to clear rows, and how to handle multiple clicks?
10+
- b-r only clears when you explicitly click a button in the popup
11+
- alternate UIs
12+
- pop up a modal after clicking the 2nd year, and have the years in the modal show as dropdowns for changing to other ranges
13+
- button to open the modal, default range is whole career
14+
- have another row below the career totals that lets you select a range
15+
- advantages of this: discoverable, no weird floating window, no confusing way to change a range after initial selection
16+
- test
17+
- baseball fielding stats, or disable
18+
- career highs
19+
- per game
20+
- totals
21+
22+
show career totals per team at bottom of stats tables in player profile pages
23+
- add some extra option to playersPlus like careerStatsPerTeam
324

425
retire players button in god mode, along with delete players in bulk actions https://discord.com/channels/290013534023057409/1333871313898573904/1333884981835075584
526

src/ui/views/Player/StatsTable.tsx

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { useState } from "react";
2+
import type { View } from "../../../common/types";
3+
import { getCols, helpers } from "../../util";
4+
import { isSport } from "../../../common";
5+
import { highlightLeaderText, MaybeBold, SeasonLink } from "./common";
6+
import { expandFieldingStats } from "../../util/expandFieldingStats.baseball";
7+
import TeamAbbrevLink from "../../components/TeamAbbrevLink";
8+
import { formatStatGameHigh } from "../PlayerStats";
9+
import SeasonIcons from "./SeasonIcons";
10+
import HideableSection from "../../components/HideableSection";
11+
import { DataTable } from "../../components";
12+
import clsx from "clsx";
13+
14+
export const StatsTable = ({
15+
name,
16+
onlyShowIf,
17+
p,
18+
stats,
19+
superCols,
20+
leaders,
21+
}: {
22+
name: string;
23+
onlyShowIf?: string[];
24+
p: View<"player">["player"];
25+
stats: string[];
26+
superCols?: any[];
27+
leaders: View<"player">["leaders"];
28+
}) => {
29+
const hasRegularSeasonStats = p.careerStats.gp > 0;
30+
const hasPlayoffStats = p.careerStatsPlayoffs.gp > 0;
31+
32+
// Show playoffs by default if that's all we have
33+
const [playoffs, setPlayoffs] = useState<boolean | "combined">(
34+
!hasRegularSeasonStats,
35+
);
36+
37+
// If game sim means we switch from having no stats to having some stats, make sure we're showing what we have
38+
if (hasRegularSeasonStats && !hasPlayoffStats && playoffs === true) {
39+
setPlayoffs(false);
40+
}
41+
if (!hasRegularSeasonStats && hasPlayoffStats && playoffs === false) {
42+
setPlayoffs(true);
43+
}
44+
45+
if (!hasRegularSeasonStats && !hasPlayoffStats) {
46+
return null;
47+
}
48+
49+
let playerStats = p.stats.filter((ps) => ps.playoffs === playoffs);
50+
const careerStats =
51+
playoffs === "combined"
52+
? p.careerStatsCombined
53+
: playoffs
54+
? p.careerStatsPlayoffs
55+
: p.careerStats;
56+
57+
if (onlyShowIf !== undefined) {
58+
let display = false;
59+
for (const stat of onlyShowIf) {
60+
if (
61+
careerStats[stat] > 0 ||
62+
(Array.isArray(careerStats[stat]) &&
63+
(careerStats[stat] as any).length > 0)
64+
) {
65+
display = true;
66+
break;
67+
}
68+
}
69+
70+
if (!display) {
71+
return null;
72+
}
73+
}
74+
75+
const cols = getCols([
76+
"Year",
77+
"Team",
78+
"Age",
79+
...stats.map((stat) =>
80+
stat === "pos"
81+
? "Pos"
82+
: `stat:${stat.endsWith("Max") ? stat.replace("Max", "") : stat}`,
83+
),
84+
]);
85+
86+
if (superCols) {
87+
superCols = helpers.deepCopy(superCols);
88+
89+
// No name
90+
superCols[0].colspan -= 1;
91+
}
92+
93+
if (isSport("basketball") && name === "Shot Locations") {
94+
cols.at(-3)!.title = "M";
95+
cols.at(-2)!.title = "A";
96+
cols.at(-1)!.title = "%";
97+
}
98+
99+
let footer;
100+
if (isSport("baseball") && name === "Fielding") {
101+
playerStats = expandFieldingStats({
102+
rows: playerStats,
103+
stats,
104+
});
105+
106+
footer = expandFieldingStats({
107+
rows: [careerStats],
108+
stats,
109+
addDummyPosIndex: true,
110+
}).map((object, i) => [
111+
i === 0 ? "Career" : null,
112+
null,
113+
null,
114+
...stats.map((stat) => formatStatGameHigh(object, stat)),
115+
]);
116+
} else {
117+
footer = [
118+
"Career",
119+
null,
120+
null,
121+
...stats.map((stat) => formatStatGameHigh(careerStats, stat)),
122+
];
123+
}
124+
125+
const leadersType =
126+
playoffs === "combined"
127+
? "combined"
128+
: playoffs === true
129+
? "playoffs"
130+
: "regularSeason";
131+
132+
let hasLeader = false;
133+
if (leadersType) {
134+
LEADERS_LOOP: for (const row of Object.values(leaders)) {
135+
if (row?.attrs.has("age")) {
136+
hasLeader = true;
137+
break;
138+
}
139+
140+
for (const stat of stats) {
141+
if (row?.[leadersType].has(stat)) {
142+
hasLeader = true;
143+
break LEADERS_LOOP;
144+
}
145+
}
146+
}
147+
}
148+
149+
const rows = [];
150+
151+
let prevSeason;
152+
for (let i = 0; i < playerStats.length; i++) {
153+
const ps = playerStats[i];
154+
155+
// Add blank rows for gap years if necessary
156+
if (prevSeason !== undefined && prevSeason < ps.season - 1) {
157+
const gapSeason = prevSeason + 1;
158+
159+
rows.push({
160+
key: `gap-${gapSeason}`,
161+
data: [
162+
{
163+
searchValue: gapSeason,
164+
165+
// i is used to index other sorts, so we need to fit in between
166+
sortValue: i - 0.5,
167+
168+
value: null,
169+
},
170+
null,
171+
null,
172+
...stats.map(() => null),
173+
],
174+
classNames: "table-secondary",
175+
});
176+
}
177+
178+
prevSeason = ps.season;
179+
180+
const className = ps.hasTot ? "text-body-secondary" : undefined;
181+
182+
rows.push({
183+
key: i,
184+
data: [
185+
{
186+
searchValue: ps.season,
187+
sortValue: i,
188+
value: (
189+
<>
190+
<SeasonLink
191+
className={className}
192+
pid={p.pid}
193+
season={ps.season}
194+
/>{" "}
195+
<SeasonIcons
196+
season={ps.season}
197+
awards={p.awards}
198+
playoffs={playoffs === true}
199+
/>
200+
</>
201+
),
202+
},
203+
<TeamAbbrevLink
204+
abbrev={ps.abbrev}
205+
className={className}
206+
season={ps.season}
207+
tid={ps.tid}
208+
/>,
209+
<MaybeBold bold={leaders[ps.season]?.attrs.has("age")}>
210+
{ps.age}
211+
</MaybeBold>,
212+
...stats.map((stat) => (
213+
<MaybeBold
214+
bold={!ps.hasTot && leaders[ps.season]?.[leadersType].has(stat)}
215+
>
216+
{formatStatGameHigh(ps, stat)}
217+
</MaybeBold>
218+
)),
219+
],
220+
classNames: className,
221+
});
222+
}
223+
224+
return (
225+
<HideableSection
226+
title={name}
227+
description={hasLeader ? highlightLeaderText : null}
228+
>
229+
<DataTable
230+
className="mb-3"
231+
cols={cols}
232+
defaultSort={[0, "asc"]}
233+
defaultStickyCols={2}
234+
footer={footer}
235+
hideAllControls
236+
name={`Player:${name}`}
237+
rows={rows}
238+
superCols={superCols}
239+
title={
240+
<ul className="nav nav-tabs border-bottom-0">
241+
{hasRegularSeasonStats ? (
242+
<li className="nav-item">
243+
<button
244+
className={clsx("nav-link", {
245+
active: playoffs === false,
246+
"border-bottom": playoffs === false,
247+
})}
248+
onClick={() => {
249+
setPlayoffs(false);
250+
}}
251+
>
252+
Regular Season
253+
</button>
254+
</li>
255+
) : null}
256+
{hasPlayoffStats ? (
257+
<li className="nav-item">
258+
<button
259+
className={clsx("nav-link", {
260+
active: playoffs === true,
261+
"border-bottom": playoffs === true,
262+
})}
263+
onClick={() => {
264+
setPlayoffs(true);
265+
}}
266+
>
267+
Playoffs
268+
</button>
269+
</li>
270+
) : null}
271+
{hasRegularSeasonStats && hasPlayoffStats ? (
272+
<li className="nav-item">
273+
<button
274+
className={clsx("nav-link", {
275+
active: playoffs === "combined",
276+
"border-bottom": playoffs === "combined",
277+
})}
278+
onClick={() => {
279+
setPlayoffs("combined");
280+
}}
281+
>
282+
Combined
283+
</button>
284+
</li>
285+
) : null}
286+
</ul>
287+
}
288+
/>
289+
</HideableSection>
290+
);
291+
};

src/ui/views/Player/common.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { JSX, ReactNode } from "react";
2+
import { helpers } from "../../util";
3+
4+
export const SeasonLink = ({
5+
className,
6+
pid,
7+
season,
8+
}: {
9+
className?: string;
10+
pid: number;
11+
season: number;
12+
}) => {
13+
return (
14+
<a
15+
className={className}
16+
href={helpers.leagueUrl(["player_game_log", pid, season])}
17+
>
18+
{season}
19+
</a>
20+
);
21+
};
22+
23+
export const highlightLeaderText = (
24+
<>
25+
<span className="highlight-leader">Bold</span> indicates league leader
26+
</>
27+
);
28+
29+
export const MaybeBold = ({
30+
bold,
31+
children,
32+
}: {
33+
bold: boolean | undefined;
34+
children: ReactNode;
35+
}) => {
36+
if (bold) {
37+
return <span className="highlight-leader">{children}</span>;
38+
}
39+
40+
return children as JSX.Element;
41+
};

0 commit comments

Comments
 (0)