From 5f620fc66c29fd1cdf0aef61142bef7dc2753a6e Mon Sep 17 00:00:00 2001 From: Jeremy Scheff Date: Fri, 31 Jan 2025 23:00:30 -0500 Subject: [PATCH] Refactor --- TODO | 21 ++ src/ui/views/Player/StatsTable.tsx | 291 +++++++++++++++++++++++++ src/ui/views/Player/common.tsx | 41 ++++ src/ui/views/Player/index.tsx | 326 +---------------------------- 4 files changed, 356 insertions(+), 323 deletions(-) create mode 100644 src/ui/views/Player/StatsTable.tsx create mode 100644 src/ui/views/Player/common.tsx diff --git a/TODO b/TODO index 0c97f9f44..e0fc460ca 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,26 @@ season range highlighting like sports-reference on click highlight - could compute stats in worker with seasonRange + - first click - highlight every row for that season + - second click - highlight every row for that season, and seasons in between +- should work regardless of sorting - still pick by season +- need to control table row highlight state outside of the table + - use normal row click feature, or build own and just pass a class down to each row? the latter might be easier +- mobile UI +- when to clear rows, and how to handle multiple clicks? + - b-r only clears when you explicitly click a button in the popup +- alternate UIs + - pop up a modal after clicking the 2nd year, and have the years in the modal show as dropdowns for changing to other ranges + - button to open the modal, default range is whole career + - have another row below the career totals that lets you select a range + - advantages of this: discoverable, no weird floating window, no confusing way to change a range after initial selection +- test + - baseball fielding stats, or disable + - career highs + - per game + - totals + +show career totals per team at bottom of stats tables in player profile pages +- add some extra option to playersPlus like careerStatsPerTeam retire players button in god mode, along with delete players in bulk actions https://discord.com/channels/290013534023057409/1333871313898573904/1333884981835075584 diff --git a/src/ui/views/Player/StatsTable.tsx b/src/ui/views/Player/StatsTable.tsx new file mode 100644 index 000000000..4ac50cf21 --- /dev/null +++ b/src/ui/views/Player/StatsTable.tsx @@ -0,0 +1,291 @@ +import { useState } from "react"; +import type { View } from "../../../common/types"; +import { getCols, helpers } from "../../util"; +import { isSport } from "../../../common"; +import { highlightLeaderText, MaybeBold, SeasonLink } from "./common"; +import { expandFieldingStats } from "../../util/expandFieldingStats.baseball"; +import TeamAbbrevLink from "../../components/TeamAbbrevLink"; +import { formatStatGameHigh } from "../PlayerStats"; +import SeasonIcons from "./SeasonIcons"; +import HideableSection from "../../components/HideableSection"; +import { DataTable } from "../../components"; +import clsx from "clsx"; + +export const StatsTable = ({ + name, + onlyShowIf, + p, + stats, + superCols, + leaders, +}: { + name: string; + onlyShowIf?: string[]; + p: View<"player">["player"]; + stats: string[]; + superCols?: any[]; + leaders: View<"player">["leaders"]; +}) => { + const hasRegularSeasonStats = p.careerStats.gp > 0; + const hasPlayoffStats = p.careerStatsPlayoffs.gp > 0; + + // Show playoffs by default if that's all we have + const [playoffs, setPlayoffs] = useState( + !hasRegularSeasonStats, + ); + + // If game sim means we switch from having no stats to having some stats, make sure we're showing what we have + if (hasRegularSeasonStats && !hasPlayoffStats && playoffs === true) { + setPlayoffs(false); + } + if (!hasRegularSeasonStats && hasPlayoffStats && playoffs === false) { + setPlayoffs(true); + } + + if (!hasRegularSeasonStats && !hasPlayoffStats) { + return null; + } + + let playerStats = p.stats.filter((ps) => ps.playoffs === playoffs); + const careerStats = + playoffs === "combined" + ? p.careerStatsCombined + : playoffs + ? p.careerStatsPlayoffs + : p.careerStats; + + if (onlyShowIf !== undefined) { + let display = false; + for (const stat of onlyShowIf) { + if ( + careerStats[stat] > 0 || + (Array.isArray(careerStats[stat]) && + (careerStats[stat] as any).length > 0) + ) { + display = true; + break; + } + } + + if (!display) { + return null; + } + } + + const cols = getCols([ + "Year", + "Team", + "Age", + ...stats.map((stat) => + stat === "pos" + ? "Pos" + : `stat:${stat.endsWith("Max") ? stat.replace("Max", "") : stat}`, + ), + ]); + + if (superCols) { + superCols = helpers.deepCopy(superCols); + + // No name + superCols[0].colspan -= 1; + } + + if (isSport("basketball") && name === "Shot Locations") { + cols.at(-3)!.title = "M"; + cols.at(-2)!.title = "A"; + cols.at(-1)!.title = "%"; + } + + let footer; + if (isSport("baseball") && name === "Fielding") { + playerStats = expandFieldingStats({ + rows: playerStats, + stats, + }); + + footer = expandFieldingStats({ + rows: [careerStats], + stats, + addDummyPosIndex: true, + }).map((object, i) => [ + i === 0 ? "Career" : null, + null, + null, + ...stats.map((stat) => formatStatGameHigh(object, stat)), + ]); + } else { + footer = [ + "Career", + null, + null, + ...stats.map((stat) => formatStatGameHigh(careerStats, stat)), + ]; + } + + const leadersType = + playoffs === "combined" + ? "combined" + : playoffs === true + ? "playoffs" + : "regularSeason"; + + let hasLeader = false; + if (leadersType) { + LEADERS_LOOP: for (const row of Object.values(leaders)) { + if (row?.attrs.has("age")) { + hasLeader = true; + break; + } + + for (const stat of stats) { + if (row?.[leadersType].has(stat)) { + hasLeader = true; + break LEADERS_LOOP; + } + } + } + } + + const rows = []; + + let prevSeason; + for (let i = 0; i < playerStats.length; i++) { + const ps = playerStats[i]; + + // Add blank rows for gap years if necessary + if (prevSeason !== undefined && prevSeason < ps.season - 1) { + const gapSeason = prevSeason + 1; + + rows.push({ + key: `gap-${gapSeason}`, + data: [ + { + searchValue: gapSeason, + + // i is used to index other sorts, so we need to fit in between + sortValue: i - 0.5, + + value: null, + }, + null, + null, + ...stats.map(() => null), + ], + classNames: "table-secondary", + }); + } + + prevSeason = ps.season; + + const className = ps.hasTot ? "text-body-secondary" : undefined; + + rows.push({ + key: i, + data: [ + { + searchValue: ps.season, + sortValue: i, + value: ( + <> + {" "} + + + ), + }, + , + + {ps.age} + , + ...stats.map((stat) => ( + + {formatStatGameHigh(ps, stat)} + + )), + ], + classNames: className, + }); + } + + return ( + + + {hasRegularSeasonStats ? ( +
  • + +
  • + ) : null} + {hasPlayoffStats ? ( +
  • + +
  • + ) : null} + {hasRegularSeasonStats && hasPlayoffStats ? ( +
  • + +
  • + ) : null} + + } + /> +
    + ); +}; diff --git a/src/ui/views/Player/common.tsx b/src/ui/views/Player/common.tsx new file mode 100644 index 000000000..c8066fa7e --- /dev/null +++ b/src/ui/views/Player/common.tsx @@ -0,0 +1,41 @@ +import type { JSX, ReactNode } from "react"; +import { helpers } from "../../util"; + +export const SeasonLink = ({ + className, + pid, + season, +}: { + className?: string; + pid: number; + season: number; +}) => { + return ( + + {season} + + ); +}; + +export const highlightLeaderText = ( + <> + Bold indicates league leader + +); + +export const MaybeBold = ({ + bold, + children, +}: { + bold: boolean | undefined; + children: ReactNode; +}) => { + if (bold) { + return {children}; + } + + return children as JSX.Element; +}; diff --git a/src/ui/views/Player/index.tsx b/src/ui/views/Player/index.tsx index b88ba27ca..71325fd36 100644 --- a/src/ui/views/Player/index.tsx +++ b/src/ui/views/Player/index.tsx @@ -1,335 +1,15 @@ -import { type ReactNode, useState, type JSX } from "react"; import { DataTable, InjuryIcon, SafeHtml, SkillsBlock } from "../../components"; import Injuries from "./Injuries"; import useTitleBar from "../../hooks/useTitleBar"; import { getCols, helpers, groupAwards } from "../../util"; import type { View } from "../../../common/types"; -import clsx from "clsx"; -import { formatStatGameHigh } from "../PlayerStats"; import SeasonIcons from "./SeasonIcons"; import TopStuff from "./TopStuff"; -import { isSport, PLAYER } from "../../../common"; -import { expandFieldingStats } from "../../util/expandFieldingStats.baseball"; +import { PLAYER } from "../../../common"; import TeamAbbrevLink from "../../components/TeamAbbrevLink"; import HideableSection from "../../components/HideableSection"; - -const SeasonLink = ({ - className, - pid, - season, -}: { - className?: string; - pid: number; - season: number; -}) => { - return ( - - {season} - - ); -}; - -const highlightLeaderText = ( - <> - Bold indicates league leader - -); - -const StatsTable = ({ - name, - onlyShowIf, - p, - stats, - superCols, - leaders, -}: { - name: string; - onlyShowIf?: string[]; - p: View<"player">["player"]; - stats: string[]; - superCols?: any[]; - leaders: View<"player">["leaders"]; -}) => { - const hasRegularSeasonStats = p.careerStats.gp > 0; - const hasPlayoffStats = p.careerStatsPlayoffs.gp > 0; - - // Show playoffs by default if that's all we have - const [playoffs, setPlayoffs] = useState( - !hasRegularSeasonStats, - ); - - // If game sim means we switch from having no stats to having some stats, make sure we're showing what we have - if (hasRegularSeasonStats && !hasPlayoffStats && playoffs === true) { - setPlayoffs(false); - } - if (!hasRegularSeasonStats && hasPlayoffStats && playoffs === false) { - setPlayoffs(true); - } - - if (!hasRegularSeasonStats && !hasPlayoffStats) { - return null; - } - - let playerStats = p.stats.filter((ps) => ps.playoffs === playoffs); - const careerStats = - playoffs === "combined" - ? p.careerStatsCombined - : playoffs - ? p.careerStatsPlayoffs - : p.careerStats; - - if (onlyShowIf !== undefined) { - let display = false; - for (const stat of onlyShowIf) { - if ( - careerStats[stat] > 0 || - (Array.isArray(careerStats[stat]) && - (careerStats[stat] as any).length > 0) - ) { - display = true; - break; - } - } - - if (!display) { - return null; - } - } - - const cols = getCols([ - "Year", - "Team", - "Age", - ...stats.map((stat) => - stat === "pos" - ? "Pos" - : `stat:${stat.endsWith("Max") ? stat.replace("Max", "") : stat}`, - ), - ]); - - if (superCols) { - superCols = helpers.deepCopy(superCols); - - // No name - superCols[0].colspan -= 1; - } - - if (isSport("basketball") && name === "Shot Locations") { - cols.at(-3)!.title = "M"; - cols.at(-2)!.title = "A"; - cols.at(-1)!.title = "%"; - } - - let footer; - if (isSport("baseball") && name === "Fielding") { - playerStats = expandFieldingStats({ - rows: playerStats, - stats, - }); - - footer = expandFieldingStats({ - rows: [careerStats], - stats, - addDummyPosIndex: true, - }).map((object, i) => [ - i === 0 ? "Career" : null, - null, - null, - ...stats.map((stat) => formatStatGameHigh(object, stat)), - ]); - } else { - footer = [ - "Career", - null, - null, - ...stats.map((stat) => formatStatGameHigh(careerStats, stat)), - ]; - } - - const leadersType = - playoffs === "combined" - ? "combined" - : playoffs === true - ? "playoffs" - : "regularSeason"; - - let hasLeader = false; - if (leadersType) { - LEADERS_LOOP: for (const row of Object.values(leaders)) { - if (row?.attrs.has("age")) { - hasLeader = true; - break; - } - - for (const stat of stats) { - if (row?.[leadersType].has(stat)) { - hasLeader = true; - break LEADERS_LOOP; - } - } - } - } - - const rows = []; - - let prevSeason; - for (let i = 0; i < playerStats.length; i++) { - const ps = playerStats[i]; - - // Add blank rows for gap years if necessary - if (prevSeason !== undefined && prevSeason < ps.season - 1) { - const gapSeason = prevSeason + 1; - - rows.push({ - key: `gap-${gapSeason}`, - data: [ - { - searchValue: gapSeason, - - // i is used to index other sorts, so we need to fit in between - sortValue: i - 0.5, - - value: null, - }, - null, - null, - ...stats.map(() => null), - ], - classNames: "table-secondary", - }); - } - - prevSeason = ps.season; - - const className = ps.hasTot ? "text-body-secondary" : undefined; - - rows.push({ - key: i, - data: [ - { - searchValue: ps.season, - sortValue: i, - value: ( - <> - {" "} - - - ), - }, - , - - {ps.age} - , - ...stats.map((stat) => ( - - {formatStatGameHigh(ps, stat)} - - )), - ], - classNames: className, - }); - } - - return ( - - - {hasRegularSeasonStats ? ( -
  • - -
  • - ) : null} - {hasPlayoffStats ? ( -
  • - -
  • - ) : null} - {hasRegularSeasonStats && hasPlayoffStats ? ( -
  • - -
  • - ) : null} - - } - /> -
    - ); -}; - -const MaybeBold = ({ - bold, - children, -}: { - bold: boolean | undefined; - children: ReactNode; -}) => { - if (bold) { - return {children}; - } - - return children as JSX.Element; -}; +import { StatsTable } from "./StatsTable"; +import { highlightLeaderText, MaybeBold, SeasonLink } from "./common"; const Player2 = ({ bestPos,