diff --git a/src/common/index.ts b/src/common/index.ts index 717a821a3b..863baf464f 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -8,7 +8,7 @@ export { default as gameAttributeHasHistory } from "./gameAttributeHasHistory"; export { default as gameAttributesArrayToObject } from "./gameAttributesArrayToObject"; export { default as getAdjustedTicketPrice } from "./getAdjustedTicketPrice"; export { default as getDraftLotteryProbs } from "./getDraftLotteryProbs"; -export { default as getCols } from "./getCols"; +export { default as getCols } from "../ui/util/columns/getCols"; export { default as getPeriodName } from "./getPeriodName"; export { default as helpers } from "./helpers"; export { default as isSport } from "./isSport"; diff --git a/src/common/types.ts b/src/common/types.ts index c9587cddfa..18c58ca9cf 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1621,6 +1621,7 @@ export type UpdateEvents = ( | "account" | "allStarDunk" | "allStarThree" + | "customizeTable" | "firstRun" | "g.goatFormula" | "gameAttributes" diff --git a/src/ui/components/BoxScore.basketball.tsx b/src/ui/components/BoxScore.basketball.tsx index 1ed4da7b5d..6ff37a1cda 100644 --- a/src/ui/components/BoxScore.basketball.tsx +++ b/src/ui/components/BoxScore.basketball.tsx @@ -21,13 +21,13 @@ const StatsTable = ({ }) => { const [sortBys, setSortBys] = useState([]); - const onClick = (event: MouseEvent, i: number) => { + const onClick = (event: MouseEvent, colKey: string) => { setSortBys(prevSortBys => { const newSortBys = updateSortBys({ cols, event, - i, + colKey, prevSortBys, }) ?? []; diff --git a/src/ui/components/BoxScore.football.tsx b/src/ui/components/BoxScore.football.tsx index fff407d217..4d426240be 100644 --- a/src/ui/components/BoxScore.football.tsx +++ b/src/ui/components/BoxScore.football.tsx @@ -39,7 +39,7 @@ export const StatsHeader = ({ sortable, }: { cols: Col[]; - onClick: (b: MouseEvent, a: number) => void; + onClick: (b: MouseEvent, a: string) => void; sortBys: SortBy[]; sortable: boolean; }) => { @@ -59,7 +59,7 @@ export const StatsHeader = ({ className={className} key={i} onClick={event => { - onClick(event, i); + onClick(event, col.key); }} title={desc} > @@ -78,7 +78,7 @@ export const sortByStats = ( ) => { return (a: any, b: any) => { for (const [index, order] of sortBys) { - const stat = stats[index]; + const stat = index.includes(":") ? index.split(":")[1] : index; const aValue = getValue?.(a, stat) ?? a.processed[stat]; const bValue = getValue?.(b, stat) ?? b.processed[stat]; @@ -109,20 +109,21 @@ const StatsTableIndividual = ({ const [sortBys, setSortBys] = useState(() => { return PLAYER_GAME_STATS[type].sortBy.map( - stat => [stats.indexOf(stat), "desc"] as SortBy, + stat => [`stat:${stat}`, "desc"] as SortBy, ); }); - const onClick = (event: MouseEvent, i: number) => { - setSortBys( - prevSortBys => + const onClick = (event: MouseEvent, colKey: string) => { + setSortBys(prevSortBys => { + return ( updateSortBys({ cols, event, - i, + colKey, prevSortBys, - }) ?? [], - ); + }) ?? [] + ); + }); }; const players = t.players diff --git a/src/ui/components/BoxScore.hockey.tsx b/src/ui/components/BoxScore.hockey.tsx index e1cd603d33..cc9fe3ac94 100644 --- a/src/ui/components/BoxScore.hockey.tsx +++ b/src/ui/components/BoxScore.hockey.tsx @@ -41,20 +41,21 @@ const StatsTable = ({ const [sortBys, setSortBys] = useState(() => { return PLAYER_GAME_STATS[type].sortBy.map( - stat => [stats.indexOf(stat), "desc"] as SortBy, + stat => [`stat:${stat}`, "desc"] as SortBy, ); }); - const onClick = (event: MouseEvent, i: number) => { - setSortBys( - prevSortBys => + const onClick = (event: MouseEvent, colKey: string) => { + setSortBys(prevSortBys => { + return ( updateSortBys({ cols, event, - i, + colKey, prevSortBys, - }) ?? [], - ); + }) ?? [] + ); + }); }; const players = t.players diff --git a/src/ui/components/DataTable/Controls.tsx b/src/ui/components/DataTable/Controls.tsx index 07d58ed981..0bf379d2bb 100644 --- a/src/ui/components/DataTable/Controls.tsx +++ b/src/ui/components/DataTable/Controls.tsx @@ -15,6 +15,7 @@ const style = { const Controls = ({ enableFilters, + enableCustomizeColumns, hideAllControls, name, onExportCSV, @@ -25,6 +26,7 @@ const Controls = ({ searchText, }: { enableFilters: boolean; + enableCustomizeColumns: boolean; hideAllControls?: boolean; name: string; onExportCSV: () => void; @@ -130,9 +132,11 @@ const Controls = ({ - - Customize Columns - + {enableCustomizeColumns ? ( + + Customize Columns + + ) : null} Download Spreadsheet diff --git a/src/ui/components/DataTable/CustomizeColumns.tsx b/src/ui/components/DataTable/CustomizeColumns.tsx index 927e150bd4..7e77283353 100644 --- a/src/ui/components/DataTable/CustomizeColumns.tsx +++ b/src/ui/components/DataTable/CustomizeColumns.tsx @@ -1,135 +1,179 @@ -import type { Col } from "."; -import { useState } from "react"; -import { Modal } from "react-bootstrap"; -import { SortableContainer, SortableElement } from "react-sortable-hoc"; -import classNames from "classnames"; +import { useEffect, useState } from "react"; +import { Dropdown, Modal } from "react-bootstrap"; +import { toWorker } from "../../util"; +import type { TableConfig } from "../../util/TableConfig"; +import { ColType, getAllCols } from "../../util/columns/getCols"; +import groupBy from "lodash-es/groupBy"; +import difference from "lodash-es/difference"; +import type { Col } from "./index"; +import { HelpPopover } from "../index"; -const Item = SortableElement( - ({ - col, - hidden, - onToggleHidden, - }: { - col?: Col; - hidden?: boolean; - onToggleHidden: () => void; - }) => { - let title; - if (col) { - title = col.title; - if (col.desc) { - title += ` (${col.desc})`; - } - if (title === "") { - title = "No Title"; - } - } else { - title = Not Currently Available; - } - - return ( -
- - -
- ); - }, -); - -const Container = SortableContainer( - ({ children, isDragged }: { children: any[]; isDragged: boolean }) => { - return ( -
    - {children} -
- ); - }, -); +export type ColConfig = Col & { + hidden: boolean; + cat: ColType; +}; const CustomizeColumns = ({ - colOrder, - cols, - hasSuperCols, onHide, - onReset, - onSortEnd, - onToggleHidden, + onSave, + config, show, }: { - colOrder: { - colIndex: number; - hidden?: boolean; - }[]; - cols: Col[]; - hasSuperCols: boolean; + config: TableConfig; onHide: () => void; - onReset: () => void; - onSortEnd: (arg: { oldIndex: number; newIndex: number }) => void; - onToggleHidden: (i: number) => () => void; + onSave: () => void; show: boolean; }) => { - const [isDragged, setIsDragged] = useState(false); + const initialColumns = () => + getAllCols().map( + (c): ColConfig => ({ + ...c, + cat: c.cat || "Other", + hidden: !config.columns.some(col => col.key === c.key), + }), + ); + const [columns, setColumns] = useState(initialColumns); + + useEffect(() => { + const nextColumns = [...columns].map(c => ({ + ...c, + hidden: !config.columns.some(col => col.key === c.key), + })); + setColumns(nextColumns); + }, [config, columns]); + + const onChange = (key: string) => () => { + const nextColumns = [...columns]; + const i = nextColumns.findIndex(c => c.key === key); + if (i !== -1) { + nextColumns[i] = { ...nextColumns[i] }; + nextColumns[i].hidden = !nextColumns[i].hidden; + setColumns(nextColumns); + } + }; + + const reset = () => setColumns(initialColumns()); + + const exit = () => { + reset(); + onHide(); + }; + + const restore = async () => { + await toWorker("main", "updateColumns", { + columns: config.fallback, + key: config.tableName, + }); + onHide(); + onSave(); + }; + + const save = async () => { + const enabledColumns: string[] = columns + .filter(c => !c.hidden) + .map(c => c.key), + currentColumns: string[] = config.columns.map(c => c.key); + + // Find columns we need to remove, and columns we need to add + const removeColumns: string[] = difference(currentColumns, enabledColumns), + addColumns: string[] = difference(enabledColumns, currentColumns); + + // Apply removals and adding to currentColumns while trying to preserve the order of currentColumns + const nextColumns: string[] = currentColumns.filter( + c => !removeColumns.includes(c), + ); + nextColumns.push(...addColumns); + + await toWorker("main", "updateColumns", { + columns: nextColumns, + key: config.tableName, + }); + onHide(); + onSave(); + }; + + const colsGrouped: { [key: string]: ColConfig[] } = groupBy( + columns, + c => c.cat, + ); return ( - - Customize Columns - -

- Click and drag to reorder columns, or use the checkboxes to show/hide - columns. -

- {hasSuperCols ? ( -

- This table has two header rows. That means you can enable/disable - columns, but not reorder them. -

- ) : null} - { - setIsDragged(true); - }} - onSortEnd={args => { - setIsDragged(false); - if (!hasSuperCols) { - onSortEnd(args); - } - }} - > - {colOrder.map(({ colIndex, hidden }, i) => { - const col = cols[colIndex]; - return ( - + + + Customize Columns + + +
    + {Object.entries(colsGrouped).map(([group, cols]) => ( +
  • +
    {group}
    +
    + {cols.map((col, i) => ( +
    +
    + + +
    +
    + ))} +
    +
  • + ))} +
- - - + + + + Reset + + + Undo Changes + Restore Default + + +
+ +

+ Undo Changes: Undo all unsaved configration changes. +

+

+ Restore Default: Restores the table's default configration, + all saved changes to this table will be lost. +

+
+
+
+ + +
); diff --git a/src/ui/components/DataTable/Footer.tsx b/src/ui/components/DataTable/Footer.tsx index c8682242b3..3316171590 100644 --- a/src/ui/components/DataTable/Footer.tsx +++ b/src/ui/components/DataTable/Footer.tsx @@ -1,16 +1,14 @@ import classNames from "classnames"; +import type { Col } from "./index"; const Footer = ({ - colOrder, + cols, footer, highlightCols, }: { - colOrder: { - colIndex: number; - hidden?: boolean; - }[]; + cols: Col[]; footer?: any[]; - highlightCols: number[]; + highlightCols: string[]; }) => { if (!footer) { return null; @@ -30,12 +28,12 @@ const Footer = ({ {footers.map((row, i) => ( - {colOrder.map(({ colIndex }, j) => { - const highlightColClassNames = highlightCols.includes(j) + {cols.map((col, j) => { + const highlightColClassNames = highlightCols.includes(col.key) ? "sorting_highlight" : undefined; - const value = row[colIndex]; + const value = row[j]; if (value != null && value.hasOwnProperty("value")) { return ( {value.value} @@ -51,7 +49,7 @@ const Footer = ({ } return ( - + {value} ); diff --git a/src/ui/components/DataTable/Header.tsx b/src/ui/components/DataTable/Header.tsx index 65d902727e..821b686af0 100644 --- a/src/ui/components/DataTable/Header.tsx +++ b/src/ui/components/DataTable/Header.tsx @@ -1,35 +1,35 @@ import classNames from "classnames"; -import type { SyntheticEvent, MouseEvent } from "react"; -import type { Col, SortBy, SuperCol } from "."; +import PropTypes from "prop-types"; +import type { MouseEvent, SyntheticEvent } from "react"; +import { ReactNode, useCallback, useState } from "react"; +import type { Col, Filter, SortBy, SuperCol } from "."; +import { + SortableContainer, + SortableElement, + SortableHandle, +} from "react-sortable-hoc"; const FilterHeader = ({ - colOrder, cols, filters, handleFilterUpdate, }: { - colOrder: { - colIndex: number; - hidden?: boolean; - }[]; cols: Col[]; - filters: string[]; - handleFilterUpdate: (b: SyntheticEvent, a: number) => void; + filters: Filter[]; + handleFilterUpdate: (b: SyntheticEvent, a: string) => void; }) => { return ( - {colOrder.map(({ colIndex }) => { - const col = cols[colIndex]; - - const filter = filters[colIndex] ?? ""; + {cols.map((col, colIndex) => { + const filter = filters.find(f => col.key === f.col); return ( {col.noSearch ? null : ( handleFilterUpdate(event, colIndex)} + onChange={event => handleFilterUpdate(event, col.key ?? "")} type="text" - value={filter} + value={filter ? filter.value : ""} /> )} @@ -39,16 +39,24 @@ const FilterHeader = ({ ); }; +FilterHeader.propTypes = { + cols: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + }), + ).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + handleFilterUpdate: PropTypes.func.isRequired, +}; + const SuperCols = ({ - colOrder, + cols, superCols, }: { - colOrder: { - colIndex: number; - }[]; + cols: Col[]; superCols: SuperCol[]; }) => { - const colIndexes = colOrder.map(x => x.colIndex); + const colIndexes = Array.from({ length: cols.length }); const maxColIndex1 = Math.max(...colIndexes); let maxColIndex2 = -1; for (const superCol of superCols) { @@ -102,14 +110,105 @@ const SuperCols = ({ ); }; -export const getSortClassName = (sortBys: SortBy[], i: number) => { +const SortableColumnHandle = SortableHandle( + (props: { isDragged: boolean; selected: boolean; children: ReactNode }) => { + return ( + + {props.children} + + ); + }, +); +SortableColumnHandle.propTypes = { + isDragged: PropTypes.bool.isRequired, +}; + +const SortableColumn = SortableElement( + (props: { + isDragged: boolean; + selected: boolean; + col: Col; + sortBy: SortBy | undefined; + colIndex: number; + handleColClick: (b: MouseEvent, a: string) => void; + }) => { + let className; + if (props.col.sortSequence && props.col.sortSequence.length === 0) { + className = null; + } else { + className = getSortClassName( + props.sortBy ? [props.sortBy] : [], + props.col.key, + ); + } + + return ( + +
+
{props.col.title}
+
{ + props.handleColClick(event, props.col.key); + }} + style={{ width: "19px" }} + className={classNames(props.col.classNames, className)} + /> +
+ + ); + }, +); +const SortableColumnHeader = SortableContainer( + (props: { + indexSelected: number | undefined; + isDragged: boolean; + cols: Col[]; + sortBys: SortBy[]; + handleColClick: (b: MouseEvent, a: string) => void; + }) => { + return ( + + {props.cols.map((col, index) => ( + sort[0] === col.key)} + /> + ))} + + ); + }, +); + +export const getSortClassName = (sortBys: SortBy[], key: string | number) => { let className = "sorting"; for (const sortBy of sortBys) { - if (sortBy[0] === i) { - className = `sorting_highlight ${ - sortBy[1] === "asc" ? "sorting_asc" : "sorting_desc" - }`; + if (sortBy[0] === key) { + className = sortBy[1] === "asc" ? "sorting_asc" : "sorting_desc"; break; } } @@ -118,66 +217,61 @@ export const getSortClassName = (sortBys: SortBy[], i: number) => { }; const Header = ({ - colOrder, cols, enableFilters, filters, handleColClick, + handleReorder, handleFilterUpdate, sortBys, superCols, }: { - colOrder: { - colIndex: number; - }[]; cols: Col[]; enableFilters: boolean; - filters: string[]; - handleColClick: (b: MouseEvent, a: number) => void; - handleFilterUpdate: (b: SyntheticEvent, a: number) => void; + filters: Filter[]; + handleColClick: (b: MouseEvent, a: string) => void; + handleReorder: (oldIndex: number, newIndex: number) => void; + handleFilterUpdate: (b: SyntheticEvent, a: string) => void; sortBys: SortBy[]; superCols?: SuperCol[]; }) => { + const [isDragged, setIsDragged] = useState(false); + const [indexSelected, setIndexSelected] = useState( + undefined, + ); + + const onSortStart = useCallback(({ index }) => { + setIsDragged(true); + setIndexSelected(index); + }, []); + + const onSortEnd = useCallback( + ({ oldIndex, newIndex }) => { + setIsDragged(false); + setIndexSelected(undefined); + + handleReorder(oldIndex, newIndex); + }, + [handleReorder], + ); + return ( - {superCols ? ( - - ) : null} - - {colOrder.map(({ colIndex }) => { - const { - classNames: colClassNames, - desc, - sortSequence, - title, - width, - } = cols[colIndex]; - - let className; - if (sortSequence && sortSequence.length === 0) { - className = null; - } else { - className = getSortClassName(sortBys, colIndex); - } - - return ( - { - handleColClick(event, colIndex); - }} - title={desc} - style={{ width }} - > - {title} - - ); - })} - + {superCols ? : null} + {enableFilters ? ( { const { clicked, toggleClicked } = useClickable(); return ( @@ -24,14 +24,16 @@ const Row = ({ })} onClick={clickable ? toggleClicked : undefined} > - {row.data.map((value = null, i) => { + {cols.map((col, i) => { + const key: string = col.key || ""; + const value = row.data[key] ?? null; // Value is either the value, or an object containing the value as a property const actualValue = value !== null && value.hasOwnProperty("value") ? value.value : value; const props: any = {}; - const highlightCol = highlightCols.includes(i); + const highlightCol = highlightCols.includes(col.key); if (value && value.classNames) { props.className = classNames( value.classNames, @@ -88,4 +90,10 @@ const Row = ({ ); }; +Row.propTypes = { + row: PropTypes.shape({ + data: PropTypes.object.isRequired, + }).isRequired, +}; + export default Row; diff --git a/src/ui/components/DataTable/index.tsx b/src/ui/components/DataTable/index.tsx index c59132ee3d..a90320fc85 100644 --- a/src/ui/components/DataTable/index.tsx +++ b/src/ui/components/DataTable/index.tsx @@ -1,16 +1,17 @@ +import type { Argument } from "classnames"; import classNames from "classnames"; import { csvFormatRows } from "d3-dsv"; import orderBy from "lodash-es/orderBy"; +import PropTypes from "prop-types"; import { - SyntheticEvent, MouseEvent, ReactNode, - useState, - useEffect, + SyntheticEvent, useCallback, + useEffect, + useState, } from "react"; import Controls from "./Controls"; -import CustomizeColumns from "./CustomizeColumns"; import Footer from "./Footer"; import Header from "./Header"; import Info from "./Info"; @@ -22,26 +23,36 @@ import getSearchVal from "./getSearchVal"; import getSortVal from "./getSortVal"; import loadStateFromCache from "./loadStateFromCache"; import ResponsiveTableWrapper from "../ResponsiveTableWrapper"; -import { downloadFile, helpers, safeLocalStorage } from "../../util"; +import { + downloadFile, + helpers, + realtimeUpdate, + safeLocalStorage, + toWorker, +} from "../../util"; import type { SortOrder, SortType } from "../../../common/types"; -import type { Argument } from "classnames"; -import { arrayMoveImmutable } from "array-move"; import type SettingsCache from "./SettingsCache"; import updateSortBys from "./updateSortBys"; +import { arrayMove } from "react-sortable-hoc"; +import CustomizeColumns from "./CustomizeColumns"; +import type { TableConfig } from "../../util/TableConfig"; -export type SortBy = [number, SortOrder]; +export type SortBy = [string, SortOrder]; export type Col = { + key: string; + title: string; classNames?: any; desc?: string; noSearch?: boolean; sortSequence?: SortOrder[]; sortType?: SortType; searchType?: SortType; - title: string; width?: string; }; +export type LegacyCol = Partial; + export type SuperCol = { colspan: number; desc?: string; @@ -49,6 +60,21 @@ export type SuperCol = { }; export type DataTableRow = { + key: number | string; + data: { + [key: string]: + | ReactNode + | { + classNames?: Argument; + value: ReactNode; + searchValue?: string; + sortValue?: string | number; + }; + }; + classNames?: Argument; +}; + +export type LegacyDataTableRow = { key: number | string; data: ( | ReactNode @@ -63,10 +89,12 @@ export type DataTableRow = { }; export type Props = { + bordered?: boolean; className?: string; clickable?: boolean; cols: Col[]; - defaultSort: SortBy; + config: TableConfig; + defaultSort?: SortBy; disableSettingsCache?: boolean; footer?: any[]; hideAllControls?: boolean; @@ -78,7 +106,17 @@ export type Props = { small?: boolean; striped?: boolean; superCols?: SuperCol[]; - addFilters?: (string | undefined)[]; + addFilters?: Filter[]; +}; + +export type LegacyProps = Omit & { + cols: LegacyCol[]; + rows: LegacyDataTableRow[]; +}; + +export type Filter = { + col: string; + value: string; }; export type State = { @@ -86,9 +124,11 @@ export type State = { colIndex: number; hidden?: boolean; }[]; + cols: Col[]; + rows: DataTableRow[]; currentPage: number; enableFilters: boolean; - filters: string[]; + filters: Filter[]; prevName: string; perPage: number; searchText: string; @@ -97,32 +137,67 @@ export type State = { settingsCache: SettingsCache; }; -const DataTable = ({ - className, - clickable = true, - cols, - defaultSort, - disableSettingsCache, - footer, - hideAllControls, - name, - nonfluid, - pagination, - rankCol, - rows, - small, - striped, - superCols, - addFilters, -}: Props) => { - const [state, setState] = useState(() => - loadStateFromCache({ +const DataTable = (props: Props | LegacyProps) => { + const { + bordered, + className, + defaultSort, + disableSettingsCache, + footer, + hideAllControls, + name, + nonfluid, + pagination, + small, + striped, + superCols, + addFilters, + } = props; + + const enableCustomizeColumns: boolean = "config" in props && !superCols; + + // Convert LegacyCols to Cols for backwards compatability + const cols: Col[] = + "config" in props + ? props.cols + : props.cols.map((col, i) => ({ + title: "", + ...col, + key: `col${i + 1}`, + })); + + // Convert LegacyDataTableRows to DataTableRows for backwards compatability + // @ts-ignore + const rows: DataTableRow[] = + props.rows.length && Array.isArray(props.rows[0].data) + ? props.rows.map( + (row): DataTableRow => ({ + ...row, + data: Array.isArray(row.data) + ? Object.fromEntries( + row.data.map((value, i) => [`col${i + 1}`, value]), + ) + : {}, + }), + ) + : props.rows; + + const [state, setState] = useState(() => ({ + ...loadStateFromCache({ cols, defaultSort, - disableSettingsCache, + disableSettingsCache: false, name, }), - ); + rows, + })); + + useEffect(() => { + if ("config" in props) { + setStatePartial({ cols }); + processedRows = processRows(); + } + }, [cols]); const setStatePartial = useCallback((newState: Partial) => { setState(state2 => ({ @@ -131,16 +206,21 @@ const DataTable = ({ })); }, []); - const processRows = () => { - const filterFunctions = state.enableFilters - ? state.filters.map((filter, i) => - createFilterFunction( - filter, - cols[i] ? cols[i].sortType : undefined, - cols[i] ? cols[i].searchType : undefined, - ), - ) - : []; + const processRows = (): DataTableRow[] => { + const filterFunctions: [string, (value: any) => boolean][] = + state.enableFilters + ? state.filters.map(filter => { + const col = state.cols.find(f => filter.col === f.key); + return [ + filter.col, + createFilterFunction( + filter.value || "", + col?.sortType, + col?.searchType, + ), + ]; + }) + : []; const skipFiltering = state.searchText === "" && !state.enableFilters; const searchText = state.searchText.toLowerCase(); const rowsFiltered = skipFiltering @@ -150,12 +230,15 @@ const DataTable = ({ if (state.searchText !== "") { let found = false; - for (let i = 0; i < row.data.length; i++) { - if (cols[i].noSearch) { + for (const col of state.cols) { + if (col.noSearch) { continue; } - if (getSearchVal(row.data[i]).includes(searchText)) { + if ( + row.data[col.key ?? ""] && + getSearchVal(row.data[col.key ?? ""]).includes(searchText) + ) { found = true; break; } @@ -168,15 +251,13 @@ const DataTable = ({ // Filter if (state.enableFilters) { - for (let i = 0; i < row.data.length; i++) { - if (cols[i].noSearch) { + for (const [key, filterFunction] of filterFunctions) { + const col = state.cols.find(c => c.key === key); + if (!col || col.noSearch) { continue; } - if ( - filterFunctions[i] && - filterFunctions[i](row.data[i]) === false - ) { + if (!filterFunction(row.data[key])) { return false; } } @@ -188,36 +269,35 @@ const DataTable = ({ const rowsOrdered = orderBy( rowsFiltered, state.sortBys.map(sortBy => row => { - let i = sortBy[0]; - - if (typeof i !== "number" || i >= row.data.length || i >= cols.length) { - i = 0; - } - - return getSortVal(row.data[i], cols[i].sortType); + const key = sortBy[0]; + const col = state.cols.find(c => c.key === key); + if (key in row.data) return getSortVal(row.data[key], col?.sortType); }), state.sortBys.map(sortBy => sortBy[1]), ); - const colOrderFiltered = state.colOrder.filter( - ({ hidden, colIndex }) => !hidden && cols[colIndex], - ); + return rowsOrdered; + }; - return rowsOrdered.map((row, i) => { - return { - ...row, - data: colOrderFiltered.map(({ colIndex }) => - colIndex === rankCol ? i + 1 : row.data[colIndex], - ), - }; + const handleReorder = async (oldIndex: number, newIndex: number) => { + const nextCols = arrayMove(state.cols, oldIndex, newIndex); + setStatePartial({ + cols: nextCols, }); + if ("config" in props) { + await toWorker("main", "updateColumns", { + columns: nextCols.map(c => c.key), + key: props.config.tableName, + }); + await realtimeUpdate(["customizeTable"]); + } }; - const handleColClick = (event: MouseEvent, i: number) => { + const handleColClick = (event: MouseEvent, colKey: string) => { const sortBys = updateSortBys({ cols, event, - i, + colKey, prevSortBys: state.sortBys, // eslint-disable-line react/no-access-state-in-setstate }); @@ -229,14 +309,12 @@ const DataTable = ({ }; const handleExportCSV = () => { - const colOrderFiltered = state.colOrder.filter( - ({ hidden, colIndex }) => !hidden && cols[colIndex], - ); - const columns = colOrderFiltered.map(({ colIndex }) => cols[colIndex]); - const colNames = columns.map(col => col.title); + const colNames = cols.map(col => col.title); const rows = processRows().map(row => - row.data.map((val, i) => { - const sortType = columns[i].sortType; + cols.map(col => { + const key: string = col.key || ""; + const val = row.data[key] ?? null; + const sortType = col.sortType; if (sortType === "currency" || sortType === "number") { return getSortVal(val, sortType, true); } @@ -284,11 +362,19 @@ const DataTable = ({ const handleFilterUpdate = ( event: SyntheticEvent, - i: number, + colKey: string, ) => { const filters = helpers.deepCopy(state.filters); // eslint-disable-line react/no-access-state-in-setstate + const filterIndex = filters.findIndex(f => colKey === f.col); + + if (filterIndex !== -1) + filters[filterIndex].value = event.currentTarget.value; + else + filters.push({ + col: colKey, + value: event.currentTarget.value, + }); - filters[i] = event.currentTarget.value; setStatePartial({ currentPage: 1, filters, @@ -325,7 +411,7 @@ const DataTable = ({ // If name changes, it means this is a whole new table and it has a different state (example: Player Stats switching between regular and advanced stats). // If colOrder does not match cols, need to run reconciliation code in loadStateFromCache (example: current vs past seasons in League Finances). - if (name !== state.prevName || cols.length > state.colOrder.length) { + if (name !== state.prevName || state.cols.length > state.colOrder.length) { setState( loadStateFromCache({ cols, @@ -337,22 +423,26 @@ const DataTable = ({ } useEffect(() => { - if ( - addFilters !== undefined && - addFilters.length === state.filters.length - ) { + if (addFilters !== undefined) { // If addFilters is passed and contains a value, merge with prevState.filters and enable filters const filters = helpers.deepCopy(state.filters); let changed = false; for (let i = 0; i < addFilters.length; i++) { - const filter = addFilters[i]; - if (filter !== undefined) { - filters[i] = filter; + const { col, value } = addFilters[i]; + const filterIndex = filters.findIndex(f => col === f.col); + + if (filterIndex !== -1) { + if (filters[filterIndex].value != value) { + changed = true; + filters[filterIndex].value = value; + } + } else { changed = true; - } else if (!state.enableFilters) { - // If there is a saved but hidden filter, remove it - filters[i] = ""; + filters.push({ + col: col, + value: value, + }); } } @@ -385,78 +475,24 @@ const DataTable = ({ processedRows = processedRows.slice(start - 1, end); } - const colOrderFiltered = state.colOrder.filter( - ({ hidden, colIndex }) => !hidden && cols[colIndex], - ); - - const highlightCols = state.sortBys - .map(sortBy => sortBy[0]) - .map(i => - colOrderFiltered.findIndex(({ colIndex }) => { - if (colIndex !== i) { - return false; - } - - // Make sure sortSequence is not an empty array - same code is in Header - const sortSequence = cols[colIndex].sortSequence; - if (sortSequence && sortSequence.length === 0) { - return false; - } - - return true; - }), - ); + const highlightCols = state.sortBys.map(sortBy => sortBy[0]); return ( <> - { - setStatePartial({ - showSelectColumnsModal: false, - }); - }} - onReset={() => { - const newOrder = cols.map((col, i) => ({ - colIndex: i, - })); - setStatePartial({ - colOrder: newOrder, - }); - state.settingsCache.set("DataTableColOrder", newOrder); - }} - onSortEnd={({ oldIndex, newIndex }) => { - const newOrder = arrayMoveImmutable( - state.colOrder, - oldIndex, - newIndex, - ); - setStatePartial({ - colOrder: newOrder, - }); - state.settingsCache.set("DataTableColOrder", newOrder); - }} - onToggleHidden={(i: number) => () => { - const newOrder = [...state.colOrder]; - if (newOrder[i]) { - newOrder[i] = { - ...newOrder[i], - }; - if (newOrder[i].hidden) { - delete newOrder[i].hidden; - } else { - newOrder[i].hidden = true; - } + {"config" in props ? ( + { setStatePartial({ - colOrder: newOrder, + showSelectColumnsModal: false, }); - state.settingsCache.set("DataTableColOrder", newOrder); - } - }} - /> + }} + onSave={async () => { + await realtimeUpdate(["customizeTable"]); + }} + /> + ) : null}
))}
@@ -542,4 +580,21 @@ const DataTable = ({ ); }; +DataTable.propTypes = { + bordered: PropTypes.bool, + className: PropTypes.string, + defaultSort: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + ), + disableSettingsCache: PropTypes.bool, + footer: PropTypes.array, + name: PropTypes.string.isRequired, + nonfluid: PropTypes.bool, + hideAllControls: PropTypes.bool, + pagination: PropTypes.bool, + rows: PropTypes.array.isRequired, + small: PropTypes.bool, + superCols: PropTypes.array, +}; + export default DataTable; diff --git a/src/ui/components/DataTable/loadStateFromCache.ts b/src/ui/components/DataTable/loadStateFromCache.ts index 1a8f7d8e6e..8cf666f10e 100644 --- a/src/ui/components/DataTable/loadStateFromCache.ts +++ b/src/ui/components/DataTable/loadStateFromCache.ts @@ -1,5 +1,5 @@ import { safeLocalStorage } from "../../util"; -import type { Props, State, SortBy } from "."; +import type { Props, State, SortBy, Filter } from "."; import SettingsCache from "./SettingsCache"; const loadStateFromCache = ({ @@ -24,19 +24,19 @@ const loadStateFromCache = ({ let sortBys: SortBy[]; if (sortBysFromStorage === undefined) { - sortBys = [defaultSort]; + sortBys = defaultSort ? [defaultSort] : []; } else { sortBys = sortBysFromStorage; } - // Don't let sortBy reference invalid col - sortBys = sortBys.filter(sortBy => sortBy[0] < cols.length); + // Don't let sortBy reference invalid + sortBys = sortBys.filter(sortBy => cols.find(col => col.key === sortBy[0])); if (sortBys.length === 0) { - sortBys = [defaultSort]; + sortBys = defaultSort ? [defaultSort] : []; } - const defaultFilters: string[] = cols.map(() => ""); + const defaultFilters: Filter[] = []; const filtersFromStorage = settingsCache.get("DataTableFilters"); let filters; @@ -47,11 +47,14 @@ const loadStateFromCache = ({ filters = filtersFromStorage; // Confirm valid filters - if (!Array.isArray(filters) || filters.length !== cols.length) { + if (!Array.isArray(filters)) { filters = defaultFilters; } else { for (const filter of filters) { - if (typeof filter !== "string") { + if ( + typeof filter.col !== "string" || + typeof filter.value !== "string" + ) { filters = defaultFilters; break; } @@ -81,6 +84,8 @@ const loadStateFromCache = ({ // If too many cols... who cares, will get filtered out return { + cols, + rows: [], colOrder, currentPage: 1, enableFilters: filters !== defaultFilters, diff --git a/src/ui/components/DataTable/updateSortBys.ts b/src/ui/components/DataTable/updateSortBys.ts index 23690b7783..96522d056b 100644 --- a/src/ui/components/DataTable/updateSortBys.ts +++ b/src/ui/components/DataTable/updateSortBys.ts @@ -5,18 +5,18 @@ import { helpers } from "../../util"; const updateSortBys = ({ cols, event, - i, + colKey, prevSortBys, }: { cols: Col[]; event: MouseEvent; - i: number; + colKey: string; prevSortBys: SortBy[]; }) => { - const col = cols[i]; + const col = cols.find(c => c.key === colKey); // Ignore click on unsortable column - if (col.sortSequence && col.sortSequence.length === 0) { + if (!col || (col.sortSequence && col.sortSequence.length === 0)) { return prevSortBys; } @@ -44,7 +44,7 @@ const updateSortBys = ({ // If this column is already in sortBys and shift is pressed, update if (event.shiftKey) { for (const sortBy of sortBys) { - if (sortBy[0] === i) { + if (sortBy[0] === colKey) { sortBy[1] = nextOrder(col, sortBy); found = true; break; @@ -53,20 +53,20 @@ const updateSortBys = ({ // If this column is not in sortBys and shift is pressed, append if (!found) { - sortBys.push([i, col.sortSequence ? col.sortSequence[0] : "asc"]); + sortBys.push([colKey, col.sortSequence ? col.sortSequence[0] : "asc"]); found = true; } } // If this column is the only one in sortBys, update order - if (!found && sortBys.length === 1 && sortBys[0][0] === i) { + if (!found && sortBys.length === 1 && sortBys[0][0] === colKey) { sortBys[0][1] = nextOrder(col, sortBys[0]); found = true; } // Otherwise, reset to sorting only by this column, default order if (!found) { - sortBys = [[i, col.sortSequence ? col.sortSequence[0] : "asc"]]; + sortBys = [[colKey, col.sortSequence ? col.sortSequence[0] : "asc"]]; } return sortBys; diff --git a/src/ui/util/TableConfig.ts b/src/ui/util/TableConfig.ts new file mode 100644 index 0000000000..194c8ead6f --- /dev/null +++ b/src/ui/util/TableConfig.ts @@ -0,0 +1,102 @@ +import { idb } from "../../worker/db"; +import getCols, { LeagueVars, MetaCol } from "./columns/getCols"; +import { cloneDeep, uniq } from "lodash-es"; +import { g } from "../../worker/util"; + +export class TableConfig { + public fallback: string[]; + public columns: MetaCol[]; + public tableName: string; + public vars?: LeagueVars; + + public statsNeeded: string[] = []; + public ratingsNeeded: string[] = []; + public attrsNeeded: string[] = ["pid"]; + + constructor( + tableName: string, + fallback: string[], + columns: MetaCol[] = [], + vars?: LeagueVars, + ) { + this.tableName = tableName; + this.fallback = fallback; + this.columns = columns; + if (vars) this.vars = vars; + } + + addColumn(column: MetaCol, pos: number) { + const colIndex = this.columns.findIndex(c => c.key === column.key); + if (colIndex !== -1) { + Object.assign(this.columns[colIndex], column); + } else { + this.columns.splice(pos, 0, column); + } + } + + updateColumn(column: Partial, key: string) { + const colIndex = this.columns.findIndex(c => c.key === key); + if (colIndex !== -1) { + Object.assign(this.columns[colIndex], column); + } + } + + setVar(key: keyof LeagueVars, value: any) { + if (this.vars) { + // @ts-ignore + this.vars[key] = value; + } + return this; + } + + static unserialize(_config: TableConfig) { + const serialized = cloneDeep(_config); + return new TableConfig( + serialized.tableName, + serialized.fallback, + serialized.columns, + serialized.vars, + ); + } + + public async load() { + const colOptions: string[] | undefined = await idb.meta.get( + "tables", + this.tableName, + ); + this.columns = getCols(colOptions ?? this.fallback); + this.statsNeeded = uniq( + this.columns.reduce( + (needed: string[], c: MetaCol) => needed.concat(c.stats ?? []), + [], + ), + ); + this.ratingsNeeded = uniq( + this.columns.reduce( + (needed: string[], c: MetaCol) => needed.concat(c.ratings ?? []), + [], + ), + ); + this.attrsNeeded = uniq( + this.columns.reduce( + (needed: string[], c: MetaCol) => needed.concat(c.attrs ?? []), + [], + ), + ); + this.vars = { + season: g.get("season"), + userTid: g.get("userTid"), + godMode: g.get("godMode"), + spectator: g.get("spectator"), + phase: g.get("phase"), + challengeNoRatings: g.get("challengeNoRatings"), + challengeNoDraftPicks: g.get("challengeNoDraftPicks"), + challengeNoFreeAgents: g.get("challengeNoFreeAgents"), + challengeNoTrades: g.get("challengeNoTrades"), + salaryCapType: g.get("salaryCapType"), + salaryCap: g.get("salaryCap"), + maxContract: g.get("maxContract"), + minContract: g.get("minContract"), + }; + } +} diff --git a/src/common/getCols.ts b/src/ui/util/columns/getCols.ts similarity index 70% rename from src/common/getCols.ts rename to src/ui/util/columns/getCols.ts index 1269b711af..043b4451b7 100644 --- a/src/common/getCols.ts +++ b/src/ui/util/columns/getCols.ts @@ -1,9 +1,52 @@ -import type { Col } from "../ui/components/DataTable"; -import bySport from "./bySport"; -import isSport from "./isSport"; +import bySport from "../../../common/bySport"; +import isSport from "../../../common/isSport"; +import type { GameAttributesLeague, Player } from "../../../common/types"; +import type { Col } from "../../components/DataTable"; -type ColTemp = Omit & { +export type ColType = + | "General" + | "Position" + | "Rating" + | "Stat" + | "Other" + | null; + +export type LeagueVars = Pick< + GameAttributesLeague, + | "userTid" + | "godMode" + | "spectator" + | "phase" + | "challengeNoRatings" + | "challengeNoDraftPicks" + | "challengeNoFreeAgents" + | "challengeNoTrades" + | "salaryCapType" + | "salaryCap" + | "maxContract" + | "minContract" +> & { + season?: string | number; +}; + +export type TemplateProps = { + p: Player; + c: MetaCol; + vars: LeagueVars; +}; + +export type MetaCol = Col & { + cat?: ColType; + ratings?: string[]; + stats?: string[]; + attrs?: string[]; + template?: string | ((props: TemplateProps) => JSX.Element | string); + options?: { [key: string]: any }; +}; + +export type ColTemp = Omit & { title?: string; + template?: string; }; const gp = isSport("hockey") ? "GP" : "G"; @@ -13,1918 +56,3100 @@ const sportSpecificCols = bySport<{ }>({ basketball: { "rating:fg": { + cat: "Rating", desc: "Mid Range", + ratings: ["fg"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "2Pt", }, "rating:tp": { + cat: "Rating", desc: "Three Pointers", + ratings: ["tp"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "3Pt", }, "rating:oiq": { + cat: "Rating", desc: "Offensive IQ", + ratings: ["oiq"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "oIQ", }, "rating:dnk": { + cat: "Rating", desc: "Dunks/Layups", + ratings: ["dnk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Dnk", }, "rating:drb": { + cat: "Rating", desc: "Dribbling", + ratings: ["drb"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Drb", }, "rating:ins": { + cat: "Rating", desc: "Inside Scoring", + ratings: ["ins"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Ins", }, "rating:jmp": { + cat: "Rating", desc: "Jumping", + ratings: ["jmp"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Jmp", }, "rating:ft": { + cat: "Rating", desc: "Free Throws", + ratings: ["ft"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "FT", }, "rating:pss": { + cat: "Rating", desc: "Passing", + ratings: ["pss"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Pss", }, "rating:reb": { + cat: "Rating", desc: "Rebounding", + ratings: ["reb"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Reb", }, "rating:diq": { + cat: "Rating", desc: "Defensive IQ", + ratings: ["diq"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "dIQ", }, "stat:2pp": { + cat: "Stat", desc: "Two Point Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["2pp"], + template: "Stat", title: "2P%", }, "stat:2p": { + cat: "Stat", desc: "Two Pointers Made", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["2p"], + template: "Stat", title: "2P", }, "stat:2pa": { + cat: "Stat", desc: "Two Pointers Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["2pa"], + template: "Stat", title: "2PA", }, "stat:pm": { + cat: "Stat", desc: "Plus/Minus", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pm"], + template: "Stat", title: "+/-", }, "stat:tpp": { + cat: "Stat", desc: "Three Point Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tpp"], + template: "Stat", title: "3P%", }, "stat:tp": { + cat: "Stat", desc: "Three Pointers Made", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tp"], + template: "Stat", title: "3P", }, "stat:tpa": { + cat: "Stat", desc: "Three Pointers Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tpa"], + template: "Stat", title: "3PA", }, "stat:tpar": { + cat: "Stat", desc: "Three Point Attempt Rate (3PA / FGA)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tpar"], + template: "Stat", title: "3PAr", }, "stat:astp": { + cat: "Stat", desc: "Percentage of teammate field goals a player assisted while on the floor", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["astp"], + template: "Stat", title: "AST%", }, "stat:ast": { + cat: "Stat", desc: "Assists", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ast"], + template: "Stat", title: "AST", }, "stat:ba": { + cat: "Stat", desc: "Blocks Against", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ba"], + template: "Stat", title: "BA", }, "stat:blk": { + cat: "Stat", desc: "Blocks", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["blk"], + template: "Stat", title: "BLK", }, "stat:blkp": { + cat: "Stat", desc: "Percentage of opponent two-pointers blocked", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["blkp"], + template: "Stat", title: "BLK%", }, "stat:drb": { + cat: "Stat", desc: "Defensive Rebounds", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["drb"], + template: "Stat", title: "DRB", }, "stat:drbp": { + cat: "Stat", desc: "Percentage of available defensive rebounds grabbed", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["drbp"], + template: "Stat", title: "DRB%", }, "stat:drtg": { + cat: "Stat", desc: "Defensive Rating (points allowed per 100 possessions)", sortSequence: ["asc", "desc"], sortType: "number", + stats: ["drtg"], + template: "Stat", title: "DRtg", }, "stat:dws": { + cat: "Stat", desc: "Defensive Win Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["dws"], + template: "Stat", title: "DWS", }, "stat:ewa": { + cat: "Stat", desc: "Estimated Wins Added", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ewa"], + template: "Stat", title: "EWA", }, "stat:efg": { + cat: "Stat", desc: "Effective Field Goal Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["efg"], + template: "Stat", title: "eFG%", }, "stat:fgp": { + cat: "Stat", desc: "Field Goal Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fgp"], + template: "Stat", title: "FG%", }, "stat:fg": { + cat: "Stat", desc: "Field Goals Made", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg"], + template: "Stat", title: "FG", }, "stat:fga": { + cat: "Stat", desc: "Field Goals Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga"], + template: "Stat", title: "FGA", }, "stat:ftp": { + cat: "Stat", desc: "Free Throw Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ftp"], + template: "Stat", title: "FT%", }, "stat:ft": { + cat: "Stat", desc: "Free Throws Made", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ft"], + template: "Stat", title: "FT", }, "stat:fta": { + cat: "Stat", desc: "Free Throws Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fta"], + template: "Stat", title: "FTA", }, "stat:ftpFga": { + cat: "Stat", desc: "Free Throws per Field Goal Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ftpFga"], + template: "Stat", title: "FTr", }, "stat:ftr": { + cat: "Stat", desc: "Free Throw Attempt Rate (FTA / FGA)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ftr"], + template: "Stat", title: "FT/FGA", }, "stat:gmsc": { + cat: "Stat", desc: "Game Score", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gmsc"], + template: "Stat", title: "GmSc", }, "stat:nrtg": { + cat: "Stat", desc: "Net Rating (point differential per 100 possessions)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["nrtg"], + template: "Stat", title: "NRtg", }, "stat:orb": { + cat: "Stat", desc: "Offensive Rebounds", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["orb"], + template: "Stat", title: "ORB", }, "stat:orbp": { + cat: "Stat", desc: "Percentage of available offensive rebounds grabbed", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["orbp"], + template: "Stat", title: "ORB%", }, "stat:ortg": { + cat: "Stat", desc: "Offensive Rating (points produced/scored per 100 possessions)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ortg"], + template: "Stat", title: "ORtg", }, "stat:ows": { + cat: "Stat", desc: "Offensive Win Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ows"], + template: "Stat", title: "OWS", }, "stat:pace": { + cat: "Stat", desc: "Possessions Per Game", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pace"], + template: "Stat", title: "Pace", }, "stat:per": { + cat: "Stat", desc: "Player Efficiency Rating", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["per"], + template: "Stat", title: "PER", }, "stat:pf": { + cat: "Stat", desc: "Personal Fouls", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pf"], + template: "Stat", title: "PF", }, - "stat:pl": { - desc: "Pythagorean Losses (expected losses based on points scored and allowed)", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "PL", - }, "stat:pts": { + cat: "Stat", desc: "Points", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pts"], + template: "Stat", title: "PTS", }, - "stat:pw": { - desc: "Pythagorean Wins (expected wins based on points scored and allowed)", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "PW", - }, "stat:stl": { + cat: "Stat", desc: "Steals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["stl"], + template: "Stat", title: "STL", }, "stat:stlp": { + cat: "Stat", desc: "Percentage of opponent possessions ending in steals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["stlp"], + template: "Stat", title: "STL%", }, "stat:tovp": { + cat: "Stat", desc: "Turnovers per 100 plays", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tovp"], + template: "Stat", title: "TOV%", }, "stat:trb": { + cat: "Stat", desc: "Total Rebounds", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["trb"], + template: "Stat", title: "TRB", }, "stat:trbp": { + cat: "Stat", desc: "Percentage of available rebounds grabbed", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["trbp"], + template: "Stat", title: "TRB%", }, "stat:tsp": { + cat: "Stat", desc: "True Shooting Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tsp"], + template: "Stat", title: "TS%", }, "stat:tov": { + cat: "Stat", desc: "Turnovers", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tov"], + template: "Stat", title: "TOV", }, "stat:usgp": { + cat: "Stat", desc: "Percentage of team plays used", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["usgp"], + template: "Stat", title: "USG%", }, "stat:ws": { + cat: "Stat", desc: "Win Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ws"], + template: "Stat", title: "WS", }, - "stat:wsPerPlayer": { - desc: "Win Shares Per Player", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "WS/Player", - }, "stat:ws48": { + cat: "Stat", desc: "Win Shares Per 48 Minutes", + options: { decimals: 3 }, sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ws48"], + template: "Stat", title: "WS/48", }, "stat:obpm": { + cat: "Stat", desc: "Offensive Box Plus-Minus", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["obpm"], + template: "Stat", title: "OBPM", }, "stat:dbpm": { + cat: "Stat", desc: "Defensive Box Plus-Minus", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["dbpm"], + template: "Stat", title: "DBPM", }, "stat:bpm": { + cat: "Stat", desc: "Box Plus-Minus", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["bpm"], + template: "Stat", title: "BPM", }, "stat:vorp": { + cat: "Stat", desc: "Value Over Replacement Player", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["vorp"], + template: "Stat", title: "VORP", }, "stat:fgAtRim": { + cat: "Stat", desc: "At Rim Made", sortSequence: ["desc", "asc"], sortType: "number", - title: "M", + stats: ["fgAtRim"], + template: "Stat", + title: "FG Rim", }, "stat:fgaAtRim": { + cat: "Stat", desc: "At Rim Attempted", sortSequence: ["desc", "asc"], sortType: "number", - title: "A", + stats: ["fgaAtRim"], + template: "Stat", + title: "FGA Rim", }, "stat:fgpAtRim": { + cat: "Stat", desc: "At Rim Percentage", sortSequence: ["desc", "asc"], sortType: "number", - title: "%", + stats: ["fgpAtRim"], + template: "Stat", + title: "FG% Rim", }, "stat:fgLowPost": { + cat: "Stat", desc: "Low Post Made", sortSequence: ["desc", "asc"], sortType: "number", - title: "M", + stats: ["fgLowPost"], + template: "Stat", + title: "FG Post", }, "stat:fgaLowPost": { + cat: "Stat", desc: "Low Post Attempted", sortSequence: ["desc", "asc"], sortType: "number", - title: "A", + stats: ["fgaLowPost"], + template: "Stat", + title: "FGA Post", }, "stat:fgpLowPost": { + cat: "Stat", desc: "Low Post Percentage", sortSequence: ["desc", "asc"], sortType: "number", - title: "%", + stats: ["fgpLowPost"], + template: "Stat", + title: "FG% Post", }, "stat:fgMidRange": { + cat: "Stat", desc: "Mid Range Made", sortSequence: ["desc", "asc"], sortType: "number", - title: "M", + stats: ["fgMidRange"], + template: "Stat", + title: "FG Mid", }, "stat:fgaMidRange": { + cat: "Stat", desc: "Mid Range Attempted", sortSequence: ["desc", "asc"], sortType: "number", - title: "A", + stats: ["fgaMidRange"], + template: "Stat", + title: "FGA Mid", }, "stat:fgpMidRange": { + cat: "Stat", desc: "Mid Range Percentage", sortSequence: ["desc", "asc"], sortType: "number", - title: "%", + stats: ["fgpMidRange"], + template: "Stat", + title: "FG% Mid", }, "stat:dd": { + cat: "Stat", desc: "Double Doubles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["dd"], + template: "Stat", title: "DD", }, "stat:td": { + cat: "Stat", desc: "Triple Doubles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["td"], + template: "Stat", title: "TD", }, "stat:qd": { + cat: "Stat", desc: "Quadruple Doubles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["qd"], + template: "Stat", title: "QD", }, "stat:fxf": { + cat: "Stat", desc: "Five by Fives", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fxf"], + template: "Stat", title: "5x5", }, }, football: { "pos:QB": { + cat: "Position", desc: "Quarterback", sortType: "number", title: "QB", }, "pos:RB": { + cat: "Position", desc: "Running Back", sortType: "number", title: "RB", }, "pos:WR": { + cat: "Position", desc: "Wide Receiver", sortType: "number", title: "WR", }, "pos:TE": { + cat: "Position", desc: "Tight End", sortType: "number", title: "TE", }, "pos:OL": { + cat: "Position", desc: "Offensive Lineman", sortType: "number", title: "OL", }, "pos:DL": { + cat: "Position", desc: "Defensive Lineman", sortType: "number", title: "DL", }, "pos:LB": { + cat: "Position", desc: "Linebacker", sortType: "number", title: "LB", }, "pos:CB": { + cat: "Position", desc: "Cornerback", sortType: "number", title: "CB", }, "pos:S": { + cat: "Position", desc: "Safety", sortType: "number", title: "S", }, "pos:K": { + cat: "Position", desc: "Kicker", sortType: "number", title: "K", }, "pos:P": { + cat: "Position", desc: "Punter", sortType: "number", title: "P", }, "rating:thv": { + cat: "Rating", desc: "Throwing Vision", + ratings: ["thv"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "ThV", }, "rating:thp": { + cat: "Rating", desc: "Throwing Power", + ratings: ["thp"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "ThP", }, "rating:tha": { + cat: "Rating", desc: "Throwing Accuracy", + ratings: ["tha"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "ThA", }, "rating:bsc": { + cat: "Rating", desc: "Ball Security", + ratings: ["bsc"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "BSc", }, "rating:elu": { + cat: "Rating", desc: "Elusiveness", + ratings: ["elu"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Elu", }, "rating:rtr": { + cat: "Rating", desc: "Route Running", + ratings: ["rtr"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "RtR", }, "rating:hnd": { + cat: "Rating", desc: "Hands", + ratings: ["hnd"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Hnd", }, "rating:rbk": { + cat: "Rating", desc: "Run Blocking", + ratings: ["rbk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "RBk", }, "rating:pbk": { + cat: "Rating", desc: "Pass Blocking", + ratings: ["pbk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PBk", }, "rating:pcv": { + cat: "Rating", desc: "Pass Coverage", + ratings: ["pcv"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PCv", }, "rating:tck": { + cat: "Rating", desc: "Tackling", + ratings: ["tck"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Tck", }, "rating:prs": { + cat: "Rating", desc: "Pass Rushing", + ratings: ["prs"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PRs", }, "rating:rns": { + cat: "Rating", desc: "Run Stopping", + ratings: ["rns"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "RnS", }, "rating:kpw": { + cat: "Rating", desc: "Kicking Power", + ratings: ["kpw"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "KPw", }, "rating:kac": { + cat: "Rating", desc: "Kicking Accuracy", + ratings: ["kac"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "KAc", }, "rating:ppw": { + cat: "Rating", desc: "Punting Power", + ratings: ["ppw"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PPw", }, "rating:pac": { + cat: "Rating", desc: "Punting Accuracy", + ratings: ["pac"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PAc", }, "rating:ovrQB": { + cat: "Rating", desc: "Overall Rating (QB)", + ratings: ["ovrQB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrQB", }, "rating:ovrRB": { + cat: "Rating", desc: "Overall Rating (RB)", + ratings: ["ovrRB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrRB", }, "rating:ovrWR": { + cat: "Rating", desc: "Overall Rating (WR)", + ratings: ["ovrWR"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrWR", }, "rating:ovrTE": { + cat: "Rating", desc: "Overall Rating (TE)", + ratings: ["ovrTE"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrTE", }, "rating:ovrOL": { + cat: "Rating", desc: "Overall Rating (OL)", + ratings: ["ovrOL"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrOL", }, "rating:ovrDL": { + cat: "Rating", desc: "Overall Rating (DL)", + ratings: ["ovrDL"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrDL", }, "rating:ovrLB": { + cat: "Rating", desc: "Overall Rating (LB)", + ratings: ["ovrLB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrLB", }, "rating:ovrCB": { + cat: "Rating", desc: "Overall Rating (CB)", + ratings: ["ovrCB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrCB", }, "rating:ovrS": { + cat: "Rating", desc: "Overall Rating (S)", + ratings: ["ovrS"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrS", }, "rating:ovrK": { + cat: "Rating", desc: "Overall Rating (K)", + ratings: ["ovrK"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrK", }, "rating:ovrP": { + cat: "Rating", desc: "Overall Rating (P)", + ratings: ["ovrP"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrP", }, "rating:ovrKR": { + cat: "Rating", desc: "Overall Rating (KR)", + ratings: ["ovrKR"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrKR", }, "rating:ovrPR": { + cat: "Rating", desc: "Overall Rating (PR)", + ratings: ["ovrPR"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrPR", }, "rating:potQB": { + cat: "Rating", desc: "Potential Rating (QB)", + ratings: ["potQB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotQB", }, "rating:potRB": { + cat: "Rating", desc: "Potential Rating (RB)", + ratings: ["potRB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotRB", }, "rating:potWR": { + cat: "Rating", desc: "Potential Rating (WR)", + ratings: ["potWR"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotWR", }, "rating:potTE": { + cat: "Rating", desc: "Potential Rating (TE)", + ratings: ["potTE"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotTE", }, "rating:potOL": { + cat: "Rating", desc: "Potential Rating (OL)", + ratings: ["potOL"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotOL", }, "rating:potDL": { + cat: "Rating", desc: "Potential Rating (DL)", + ratings: ["potDL"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotDL", }, "rating:potLB": { + cat: "Rating", desc: "Potential Rating (LB)", + ratings: ["potLB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotLB", }, "rating:potCB": { + cat: "Rating", desc: "Potential Rating (CB)", + ratings: ["potCB"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotCB", }, "rating:potS": { + cat: "Rating", desc: "Potential Rating (S)", + ratings: ["potS"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotS", }, "rating:potK": { + cat: "Rating", desc: "Potential Rating (K)", + ratings: ["potK"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotK", }, "rating:potP": { + cat: "Rating", desc: "Potential Rating (P)", + ratings: ["potP"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotP", }, "rating:potKR": { + cat: "Rating", desc: "Potential Rating (KR)", + ratings: ["potKR"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotKR", }, "rating:potPR": { + cat: "Rating", desc: "Potential Rating (PR)", + ratings: ["potPR"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotPR", }, "stat:fmb": { + cat: "Stat", desc: "Fumbles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fmb"], + template: "Stat", title: "Fmb", }, "stat:fmbLost": { + cat: "Stat", desc: "Fumbles Lost", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fmbLost"], + template: "Stat", title: "FL", }, "stat:fp": { + cat: "Stat", desc: "Fantasy Points", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fp"], + template: "Stat", title: "FP", }, "stat:pssCmp": { + cat: "Stat", desc: "Completions", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssCmp"], + template: "Stat", title: "Cmp", }, "stat:pss": { + cat: "Stat", desc: "Passing Attempts", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pss"], + template: "Stat", title: "Att", }, "stat:pssYds": { + cat: "Stat", desc: "Passing Yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssYds"], + template: "Stat", title: "Yds", }, "stat:pssTD": { + cat: "Stat", desc: "Passing Touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssTD"], + template: "Stat", title: "TD", }, "stat:pssInt": { + cat: "Stat", desc: "Interceptions", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssInt"], + template: "Stat", title: "Int", }, "stat:pssLng": { + cat: "Stat", desc: "Longest Pass", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssLng"], + template: "Stat", title: "Lng", }, "stat:pssSk": { + cat: "Stat", desc: "Times Sacked", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssSk"], + template: "Stat", title: "Sk", }, "stat:pssSkYds": { + cat: "Stat", desc: "Yards lost due to sacks", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssSkYds"], + template: "Stat", title: "Yds", }, "stat:rus": { + cat: "Stat", desc: "Rushing Attempts", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rus"], + template: "Stat", title: "Rush", }, "stat:rusYds": { + cat: "Stat", desc: "Rushing Yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusYds"], + template: "Stat", title: "Yds", }, "stat:rusTD": { + cat: "Stat", desc: "Rushing Touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusTD"], + template: "Stat", title: "TD", }, "stat:rusLng": { + cat: "Stat", desc: "Longest Run", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusLng"], + template: "Stat", title: "Lng", }, "stat:tgt": { + cat: "Stat", desc: "Targets", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tgt"], + template: "Stat", title: "Tgt", }, "stat:rec": { + cat: "Stat", desc: "Receptions", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rec"], + template: "Stat", title: "Rec", }, "stat:recYds": { + cat: "Stat", desc: "Receiving Yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recYds"], + template: "Stat", title: "Yds", }, "stat:recTD": { + cat: "Stat", desc: "Receiving Touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recTD"], + template: "Stat", title: "TD", }, "stat:recLng": { + cat: "Stat", desc: "Longest Reception", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recLng"], + template: "Stat", title: "Lng", }, "stat:pr": { + cat: "Stat", desc: "Punt Returns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pr"], + template: "Stat", title: "PR", }, "stat:prYds": { + cat: "Stat", desc: "Punt Return Yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["prYds"], + template: "Stat", title: "Yds", }, "stat:prTD": { + cat: "Stat", desc: "Punts returned for touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["prTD"], + template: "Stat", title: "TD", }, "stat:prLng": { + cat: "Stat", desc: "Longest Punt Return", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["prLng"], + template: "Stat", title: "Lng", }, "stat:kr": { + cat: "Stat", desc: "Kickoff Returns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["kr"], + template: "Stat", title: "KR", }, "stat:krYds": { + cat: "Stat", + template: "Stat", + stats: ["krYds"], desc: "Kickoff Return Yards", sortSequence: ["desc", "asc"], sortType: "number", title: "Yds", }, "stat:krTD": { + cat: "Stat", desc: "Kickoffs returned for touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["krTD"], + template: "Stat", title: "TD", }, "stat:krLng": { + cat: "Stat", desc: "Longest Kickoff Return", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["krLng"], + template: "Stat", title: "Lng", }, "stat:defInt": { + cat: "Stat", desc: "Interceptions", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defInt"], + template: "Stat", title: "Int", }, "stat:defIntYds": { + cat: "Stat", desc: "Yards interceptions were returned for", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defIntYds"], + template: "Stat", title: "Yds", }, "stat:defIntTD": { + cat: "Stat", desc: "Interceptions returned for touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defIntTD"], + template: "Stat", title: "TD", }, "stat:defIntLng": { + cat: "Stat", desc: "Longest Interception Return", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defIntLng"], + template: "Stat", title: "Lng", }, "stat:defPssDef": { + cat: "Stat", desc: "Passes Defended", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defPssDef"], + template: "Stat", title: "PD", }, "stat:defFmbFrc": { + cat: "Stat", desc: "Forced Fumbles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defFmbFrc"], + template: "Stat", title: "FF", }, "stat:defFmbRec": { + cat: "Stat", desc: "Fumbles Recovered", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defFmbRec"], + template: "Stat", title: "FR", }, "stat:defFmbYds": { + cat: "Stat", desc: "Yards fumbles were returned for", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defFmbYds"], + template: "Stat", title: "Yds", }, "stat:defFmbTD": { + cat: "Stat", desc: "Fumbles returned for touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defFmbTD"], + template: "Stat", title: "TD", }, "stat:defFmbLng": { + cat: "Stat", desc: "Longest Fumble Return", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defFmbLng"], + template: "Stat", title: "Lng", }, "stat:defSk": { + cat: "Stat", desc: "Sacks", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defSk"], + template: "Stat", title: "Sk", }, "stat:defTckSolo": { + cat: "Stat", desc: "Solo Tackles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defTckSolo"], + template: "Stat", title: "Solo", }, "stat:defTckAst": { + cat: "Stat", desc: "Assists On Tackles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defTckAst"], + template: "Stat", title: "Ast", }, "stat:defTckLoss": { + cat: "Stat", desc: "Tackes For Loss", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defTckLoss"], + template: "Stat", title: "TFL", }, "stat:defSft": { + cat: "Stat", desc: "Safeties Scored", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defSft"], + template: "Stat", title: "Sfty", }, "stat:fg0": { + cat: "Stat", desc: "Field Goals Made, 19 yards and under", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg0"], + template: "Stat", title: "FG10", }, "stat:fga0": { + cat: "Stat", desc: "Field Goals Attempted, 19 yards and under", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga0"], + template: "Stat", title: "FGA10", }, "stat:fg20": { + cat: "Stat", desc: "Field Goals Made, 20-29 yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg20"], + template: "Stat", title: "FG20", }, "stat:fga20": { + cat: "Stat", desc: "Field Goals Attempted, 20-29 yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga20"], + template: "Stat", title: "FGA20", }, "stat:fg30": { + cat: "Stat", desc: "Field Goals Made, 30-39 yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg30"], + template: "Stat", title: "FG30", }, "stat:fga30": { + cat: "Stat", desc: "Field Goals Attempted, 30-39 yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga30"], + template: "Stat", title: "FGA30", }, "stat:fg40": { + cat: "Stat", desc: "Field Goals Made, 40-49 yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg40"], + template: "Stat", title: "FG40", }, "stat:fga40": { + cat: "Stat", desc: "Field Goals Attempted, 40-49 yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga40"], + template: "Stat", title: "FGA40", }, "stat:fg50": { + cat: "Stat", desc: "Field Goals Made, 50+ yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg50"], + template: "Stat", title: "FG50", }, "stat:fga50": { + cat: "Stat", desc: "Field Goals Attempted, 50+ yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga50"], + template: "Stat", title: "FGA50", }, "stat:fgLng": { + cat: "Stat", desc: "Longest Field Goal", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fgLng"], + template: "Stat", title: "Lng", }, "stat:xp": { + cat: "Stat", desc: "Extra Points Made", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["xp"], + template: "Stat", title: "XPM", }, "stat:xpa": { + cat: "Stat", desc: "Extra Points Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["xpa"], + template: "Stat", title: "XPA", }, "stat:pnt": { + cat: "Stat", desc: "Times Punted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pnt"], + template: "Stat", title: "Pnt", }, "stat:pntYds": { + cat: "Stat", desc: "Total Punt Yardage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pntYds"], + template: "Stat", title: "Yds", }, "stat:pntLng": { + cat: "Stat", desc: "Longest Punt", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pntLng"], + template: "Stat", title: "Lng", }, "stat:pntBlk": { + cat: "Stat", desc: "Times Punts Blocked", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pntBlk"], + template: "Stat", title: "Blk", }, "stat:pen": { + cat: "Stat", desc: "Penalties", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pen"], + template: "Stat", title: "Pen", }, "stat:penYds": { + cat: "Stat", desc: "Penalty Yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["penYds"], + template: "Stat", title: "Yds", }, "stat:cmpPct": { + cat: "Stat", desc: "Completion Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["cmpPct"], + template: "Stat", title: "Pct", }, "stat:qbRat": { + cat: "Stat", desc: "Quarterback Rating", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["qbRat"], + template: "Stat", title: "QBRat", }, "stat:rusYdsPerAtt": { + cat: "Stat", desc: "Rushing Yards Per Attempt", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusYdsPerAtt"], + template: "Stat", title: "Y/A", }, "stat:recYdsPerAtt": { + cat: "Stat", desc: "Yards Per Catch", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recYdsPerAtt"], + template: "Stat", title: "Y/A", }, "stat:fg": { + cat: "Stat", desc: "Field Goals Made", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fg"], + template: "Stat", title: "FGM", }, "stat:fga": { + cat: "Stat", desc: "Field Goals Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fga"], + template: "Stat", title: "FGA", }, "stat:fgPct": { + cat: "Stat", desc: "Field Goal Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fgPct"], + template: "Stat", title: "Pct", }, "stat:xpPct": { + cat: "Stat", desc: "Extra Point Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["xpPct"], + template: "Stat", title: "Pct", }, "stat:kickingPts": { + cat: "Stat", desc: "Kicking Points", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["kickingPts"], + template: "Stat", title: "Pts", }, "stat:pntYdsPerAtt": { + cat: "Stat", desc: "Yards Per Punt", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pntYdsPerAtt"], + template: "Stat", title: "Y/A", }, "stat:pntTB": { + cat: "Stat", desc: "Punt Touchbacks", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pntTB"], + template: "Stat", title: "TB", }, "stat:pntIn20": { + cat: "Stat", desc: "Punts Inside 20", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pntIn20"], + template: "Stat", title: "In20", }, "stat:krYdsPerAtt": { + cat: "Stat", desc: "Yards Per Kick Return", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["krYdsPerAtt"], + template: "Stat", title: "Y/A", }, "stat:prYdsPerAtt": { + cat: "Stat", desc: "Yards Per Punt Return", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["prYdsPerAtt"], + template: "Stat", title: "Y/A", }, "stat:defTck": { + cat: "Stat", desc: "Total Tackles", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["defTck"], + template: "Stat", title: "Tck", }, "stat:keyStats": { + cat: "Stat", desc: "Key Stats", sortSequence: ["desc", "asc"], sortType: "string", + stats: ["keyStats"], + template: "Stat", title: "Stats", }, "stat:pts": { + cat: "Stat", desc: "", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pts"], + template: "Stat", title: "Pts", }, "stat:yds": { + cat: "Stat", desc: "Offensive Yards", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["yds"], + template: "Stat", title: "Yds", }, "stat:ply": { + cat: "Stat", desc: "Plays", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ply"], + template: "Stat", title: "Ply", }, "stat:ydsPerPlay": { + cat: "Stat", desc: "Yards Per Play", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ydsPerPlay"], + template: "Stat", title: "Y/P", }, "stat:tov": { + cat: "Stat", desc: "Turnovers", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tov"], + template: "Stat", title: "TO", }, "stat:drives": { + cat: "Stat", desc: "Number of Drives", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["drives"], + template: "Stat", title: "#Dr", }, "stat:drivesScoringPct": { + cat: "Stat", desc: "Percentage of drives ending in a score", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["drivesScoringPct"], + template: "Stat", title: "Sc%", }, "stat:drivesTurnoverPct": { + cat: "Stat", desc: "Percentage of drives ending in a turnover", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["drivesTurnoverPct"], + template: "Stat", title: "TO%", }, "stat:avgFieldPosition": { + cat: "Stat", desc: "Average Starting Field Position", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["avgFieldPosition"], + template: "Stat", title: "Start", }, "stat:timePerDrive": { + cat: "Stat", desc: "Time Per Drive (minutes)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["timePerDrive"], + template: "Stat", title: "Tm/D", }, "stat:playsPerDrive": { + cat: "Stat", desc: "Number of Plays Per Drive", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["playsPerDrive"], + template: "Stat", title: "Ply/D", }, "stat:ydsPerDrive": { + cat: "Stat", desc: "Yards Per Drive", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ydsPerDrive"], + template: "Stat", title: "Y/D", }, "stat:ptsPerDrive": { + cat: "Stat", desc: "Points Per Drive", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ptsPerDrive"], + template: "Stat", title: "Pts/D", }, "stat:qbRec": { + cat: "Stat", desc: "Record as primary QB", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["qbRec"], + template: "Stat", title: "QBRec", }, "stat:qbW": { + cat: "Stat", desc: "Wins as primary QB", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["qbW"], + template: "Stat", title: "QBW", }, "stat:qbL": { + cat: "Stat", desc: "Losses as primary QB", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["qbL"], + template: "Stat", title: "QBL", }, "stat:qbT": { + cat: "Stat", desc: "Ties as primary QB", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["qbT"], + template: "Stat", title: "QBT", }, "stat:qbOTL": { + cat: "Stat", desc: "Overtime losses as primary QB", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["qbOTL"], + template: "Stat", title: "QBOTL", }, "stat:pssTDPct": { + cat: "Stat", desc: "Percentage of passes that result in touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssTDPct"], + template: "Stat", title: "TD%", }, "stat:pssIntPct": { + cat: "Stat", desc: "Percentage of passes that result in interceptions", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssIntPct"], + template: "Stat", title: "Int%", }, "stat:pssYdsPerAtt": { + cat: "Stat", desc: "Pass Yards Per Attempt", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssYdsPerAtt"], + template: "Stat", title: "Y/A", }, "stat:pssAdjYdsPerAtt": { + cat: "Stat", desc: "Adjusted Pass Yards Per Attempt ((yds + 20 * TD - 45 * int) / att)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssAdjYdsPerAtt"], + template: "Stat", title: "AY/A", }, "stat:pssYdsPerCmp": { + cat: "Stat", desc: "Pass Yards Per Completion", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssYdsPerCmp"], + template: "Stat", title: "Y/C", }, "stat:pssYdsPerGame": { + cat: "Stat", desc: "Pass Yards Per Game", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssYdsPerGame"], + template: "Stat", title: "Y/G", }, "stat:pssNetYdsPerAtt": { + cat: "Stat", desc: "Net Pass Yards Per Attempt (passes and sacks)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssNetYdsPerAtt"], + template: "Stat", title: "NY/A", }, "stat:pssAdjNetYdsPerAtt": { + cat: "Stat", desc: "Adjusted Net Pass Yards Per Attempt ((yds + 20 * TD - 45 * int - skYds) / (att + sk))", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssAdjNetYdsPerAtt"], + template: "Stat", title: "ANY/A", }, "stat:pssSkPct": { + cat: "Stat", desc: "Percentage of times sacked when attempting a pass", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pssSkPct"], + template: "Stat", title: "Sk%", }, "stat:rusYdsPerGame": { + cat: "Stat", desc: "Rushing Yards Per Game", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusYdsPerGame"], + template: "Stat", title: "Y/G", }, "stat:rusPerGame": { + cat: "Stat", desc: "Rushing Attempts Per Game", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusPerGame"], + template: "Stat", title: "A/G", }, "stat:recYdsPerRec": { + cat: "Stat", desc: "Yards Per Reception", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recYdsPerRec"], + template: "Stat", title: "Y/R", }, "stat:recPerGame": { + cat: "Stat", desc: "Receptions Per Game", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recPerGame"], + template: "Stat", title: "R/G", }, "stat:recYdsPerGame": { + cat: "Stat", desc: "Receiving Yards Per Game", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recYdsPerGame"], + template: "Stat", title: "Y/G", }, "stat:recCatchPct": { + cat: "Stat", desc: "Catch Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["recCatchPct"], + template: "Stat", title: "Ctch%", }, "stat:touches": { + cat: "Stat", desc: "Touches (Rushing Attempts And Receptions)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["touches"], + template: "Stat", title: "Tch", }, "stat:ydsPerTouch": { + cat: "Stat", desc: "Yards Per Touch", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ydsPerTouch"], + template: "Stat", title: "Y/Tch", }, "stat:ydsFromScrimmage": { + cat: "Stat", desc: "Total Rushing and Receiving Yards From Scrimmage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ydsFromScrimmage"], + template: "Stat", title: "YScm", }, "stat:rusRecTD": { + cat: "Stat", desc: "Total Rushing and Receiving Touchdowns", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["rusRecTD"], + template: "Stat", title: "RRTD", }, "stat:allPurposeYds": { + cat: "Stat", desc: "All Purpose Yards (Rushing, Receiving, and Kick/Punt/Fumble/Interception Returns)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["allPurposeYds"], + template: "Stat", title: "APY", }, "stat:av": { + cat: "Stat", desc: "Approximate Value", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["av"], + template: "Stat", title: "AV", }, "stat:avPerPlayer": { + cat: "Stat", desc: "Approximate Value Per Player", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["avPerPlayer"], + template: "Stat", title: "AV/Player", }, }, hockey: { "pos:C": { + cat: "Position", desc: "Center", sortType: "number", title: "C", }, "pos:W": { + cat: "Position", desc: "Wing", sortType: "number", title: "W", }, "pos:D": { + cat: "Position", desc: "Defenseman", sortType: "number", title: "D", }, "pos:G": { + cat: "Position", desc: "Goalie", sortType: "number", title: "G", }, "rating:pss": { + cat: "Rating", desc: "Passing", + ratings: ["pss"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Pss", }, "rating:wst": { + cat: "Rating", desc: "Wristshot", + ratings: ["wst"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Wst", }, "rating:sst": { + cat: "Rating", desc: "Slapshot", + ratings: ["sst"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Sst", }, "rating:stk": { + cat: "Rating", desc: "Stickhandling", + ratings: ["stk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Stk", }, "rating:oiq": { + cat: "Rating", desc: "Offensive IQ", + ratings: ["oiq"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "oIQ", }, "rating:chk": { + cat: "Rating", desc: "Checking", + ratings: ["chk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Chk", }, "rating:blk": { + cat: "Rating", desc: "Shot Blocking", + ratings: ["blk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Blk", }, "rating:fcf": { + cat: "Rating", desc: "Faceoffs", + ratings: ["fcf"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Fcf", }, "rating:diq": { + cat: "Rating", desc: "Defensive IQ", + ratings: ["diq"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "dIQ", }, "rating:glk": { + cat: "Rating", desc: "Goalkeeping", + ratings: ["glk"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "Glk", }, "rating:ovrC": { + cat: "Rating", desc: "Overall Rating (Center)", + ratings: ["ovrC"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrC", }, "rating:ovrW": { + cat: "Rating", desc: "Overall Rating (Winger)", + ratings: ["ovrW"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrW", }, "rating:ovrD": { + cat: "Rating", desc: "Overall Rating (Defenseman)", + ratings: ["ovrD"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrD", }, "rating:ovrG": { + cat: "Rating", desc: "Overall Rating (Goalie)", + ratings: ["ovrG"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "OvrG", }, "rating:potC": { + cat: "Rating", desc: "Potential Rating (Center)", + ratings: ["potC"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotC", }, "rating:potW": { + cat: "Rating", desc: "Potential Rating (Winger)", + ratings: ["potW"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotW", }, "rating:potD": { + cat: "Rating", desc: "Potential Rating (Defenseman)", + ratings: ["potD"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotD", }, "rating:potG": { + cat: "Rating", desc: "Potential Rating (Goalie)", + ratings: ["potG"], sortSequence: ["desc", "asc"], sortType: "number", + template: "Rating", title: "PotG", }, "stat:gpGoalie": { + cat: "Stat", desc: "Games Played (Goalie)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gpGoalie"], + template: "Stat", title: gp, }, "stat:gpSkater": { + cat: "Stat", desc: "Games Played (Skater)", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gpSkater"], + template: "Stat", title: gp, }, "stat:pm": { + cat: "Stat", desc: "Plus/Minus", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pm"], + template: "Stat", title: "+/-", }, "stat:pim": { + cat: "Stat", desc: "Penalty Minutes", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pim"], + template: "Stat", title: "PIM", }, "stat:evG": { + cat: "Stat", desc: "Even Strength Goals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["evG"], + template: "Stat", title: "evG", }, "stat:ppG": { + cat: "Stat", desc: "Power Play Goals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ppG"], + template: "Stat", title: "ppG", }, "stat:shG": { + cat: "Stat", desc: "Short-Handed Goals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["shG"], + template: "Stat", title: "shG", }, "stat:gwG": { + cat: "Stat", desc: "Game Winning Goals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gwG"], + template: "Stat", title: "gwG", }, "stat:evA": { + cat: "Stat", desc: "Even Strength Assists", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["evA"], + template: "Stat", title: "evA", }, "stat:ppA": { + cat: "Stat", desc: "Power Play Assists", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ppA"], + template: "Stat", title: "ppA", }, "stat:shA": { + cat: "Stat", desc: "Short-Handed Assists", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["shA"], + template: "Stat", title: "shA", }, "stat:gwA": { + cat: "Stat", desc: "Game Winning Assists", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gwA"], + template: "Stat", title: "gwA", }, "stat:s": { + cat: "Stat", desc: "Shots on Goal", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["s"], + template: "Stat", title: "S", }, "stat:tsa": { + cat: "Stat", desc: "Total Shots Attempted", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tsa"], + template: "Stat", title: "TSA", }, "stat:fow": { + cat: "Stat", desc: "Faceoff Wins", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fow"], + template: "Stat", title: "FOW", }, "stat:fol": { + cat: "Stat", desc: "Faceoff Losses", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["fol"], + template: "Stat", title: "FOL", }, "stat:foPct": { + cat: "Stat", desc: "Faceoff Win Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["foPct"], + template: "Stat", title: "FO%", }, "stat:blk": { + cat: "Stat", desc: "Blocks", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["blk"], + template: "Stat", title: "BLK", }, "stat:hit": { + cat: "Stat", desc: "Hits", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["hit"], + template: "Stat", title: "HIT", }, "stat:tk": { + cat: "Stat", desc: "Takeaways", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["tk"], + template: "Stat", title: "TK", }, "stat:gv": { + cat: "Stat", desc: "Giveaways", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gv"], + template: "Stat", title: "GV", }, "stat:ga": { + cat: "Stat", desc: "Goals Against", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ga"], + template: "Stat", title: "GA", }, "stat:sv": { + cat: "Stat", desc: "Saves", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["sv"], + template: "Stat", title: "SV", }, "stat:so": { + cat: "Stat", desc: "Shutouts", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["so"], + template: "Stat", title: "SO", }, "stat:g": { + cat: "Stat", desc: "Goals", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["g"], + template: "Stat", title: "G", }, "stat:a": { + cat: "Stat", desc: "Assists", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["a"], + template: "Stat", title: "A", }, "stat:pts": { + cat: "Stat", desc: "Points", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["pts"], + template: "Stat", title: "PTS", }, "stat:sPct": { + cat: "Stat", desc: "Shooting Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["sPct"], + template: "Stat", title: "S%", }, "stat:svPct": { + cat: "Stat", desc: "Save Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["svPct"], + template: "Stat", title: "SV%", }, "stat:sa": { + cat: "Stat", desc: "Shots Against", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["sa"], + template: "Stat", title: "SA", }, "stat:gaa": { + cat: "Stat", desc: "Goals Against Average", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gaa"], + template: "Stat", title: "GAA", }, "stat:keyStats": { + cat: "Stat", desc: "Key Stats", sortSequence: ["desc", "asc"], sortType: "string", + stats: ["keyStats"], + template: "Stat", title: "Stats", }, "stat:ps": { + cat: "Stat", desc: "Point Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ps"], + template: "Stat", title: "PS", }, "stat:psPerPlayer": { + cat: "Stat", desc: "Point Shares Per Player", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["psPerPlayer"], + template: "Stat", title: "PS/Player", }, "stat:ops": { + cat: "Stat", desc: "Offensive Point Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ops"], + template: "Stat", title: "OPS", }, "stat:dps": { + cat: "Stat", desc: "Defensive Point Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["dps"], + template: "Stat", title: "DPS", }, "stat:gps": { + cat: "Stat", desc: "Goalie Point Shares", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gps"], + template: "Stat", title: "GPS", }, "stat:gc": { + cat: "Stat", desc: "Goals Created", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["gc"], + template: "Stat", title: "GC", }, "stat:amin": { + cat: "Stat", desc: "Average Time On Ice", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["amin"], + template: "Stat", title: "ATOI", }, "stat:ppMin": { + cat: "Stat", desc: "Power Play Time On Ice", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ppMin"], + template: "Stat", title: "ppTOI", }, "stat:shMin": { + cat: "Stat", desc: "Short Handed Time On Ice", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["shMin"], + template: "Stat", title: "shTOI", }, "stat:ppo": { + cat: "Stat", desc: "Power Play Opportunities", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ppo"], + template: "Stat", title: "PPO", }, "stat:ppPct": { + cat: "Stat", desc: "Power Play Percentage", sortSequence: ["desc", "asc"], sortType: "number", + stats: ["ppPct"], + template: "Stat", title: "PP%", }, "stat:gRec": { + cat: "Stat", desc: "Record as primary G", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["gRec"], + template: "Stat", title: "Rec", }, "stat:gW": { + cat: "Stat", desc: "Wins as primary G", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["gW"], + template: "Stat", title: "GW", }, "stat:gL": { + cat: "Stat", desc: "Losses as primary G", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["gL"], + template: "Stat", title: "GL", }, "stat:gT": { + cat: "Stat", desc: "Ties as primary G", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["gT"], + template: "Stat", title: "GT", }, "stat:gOTL": { + cat: "Stat", desc: "Overtime losses as primary G", sortSequence: ["desc", "asc"], sortType: "record", + stats: ["gOTL"], + template: "Stat", title: "GOTL", }, }, }); const cols: { [key: string]: ColTemp; +} = { + Acquired: { + cat: "General", + desc: "How Player Was Acquired", + }, + Age: { + attrs: ["age"], + cat: "General", + sortType: "number", + template: "Age", + }, + "Asking For": { + attrs: ["contract", "mood"], + cat: "General", + sortSequence: ["desc", "asc"], + sortType: "currency", + template: "AskingFor", + }, + College: { + attrs: ["college"], + cat: "General", + template: "College", + }, + Contract: { + attrs: ["contract"], + cat: "General", + sortSequence: ["desc", "asc"], + sortType: "currency", + template: "Contract", + options: { format: "compact" }, + }, + FullContract: { + title: "Full Contract", + attrs: ["contract"], + cat: "General", + sortSequence: ["desc", "asc"], + sortType: "currency", + template: "Contract", + options: { format: "full" }, + }, + Country: { + attrs: ["born"], + cat: "General", + template: "Country", + }, + Projected: { + attrs: ["contractDesired"], + cat: "General", + desc: "Projected Contract Demand", + sortSequence: ["desc", "asc"], + sortType: "currency", + template: "Projected", + }, + Mood: { + attrs: ["mood"], + cat: "General", + desc: "How Player Feels About Your Team", + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Mood", + width: "1px", + }, + CurrentMood: { + attrs: ["mood"], + cat: "General", + desc: "How Player Feels About Their Current Team", + sortSequence: ["desc", "asc"], + sortType: "number", + template: "MoodCurrent", + title: "Current Mood", + width: "1px", + }, + DraftYear: { + attrs: ["draft"], + cat: "General", + sortType: "number", + template: "DraftYear", + title: "Draft Year", + }, + Name: { + cat: "General", + ratings: ["skills"], + attrs: ["pid", "name", "injury", "jerseyNumber", "watch"], + sortType: "name", + template: "Name", + }, + Exp: { + cat: "General", + desc: "Contract Expiration", + attrs: ["contract"], + sortSequence: ["asc", "desc"], + sortType: "number", + template: "Exp", + }, + Experience: { + cat: "General", + desc: "Number of Years in the League", + attrs: ["experience"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Experience", + }, + Ovr: { + cat: "General", + desc: "Overall Rating", + ratings: ["dovr", "ovr"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Ovr", + }, + "Ovr Drop": { + cat: "General", + desc: "Decrease in Overall Rating", + sortSequence: ["desc", "asc"], + sortType: "number", + template: "attr", + attrs: ["ovrDrop"], + }, + "Peak Ovr": { + cat: "General", + desc: "Peak Overall Rating", + sortSequence: ["desc", "asc"], + sortType: "number", + template: "attr", + attrs: ["peakOvr"], + }, + Pos: { + cat: "General", + desc: "Position", + ratings: ["pos"], + attrs: ["pos"], + template: "Pos", + }, + Pot: { + cat: "General", + desc: "Potential Rating", + ratings: ["dpot", "pot"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Pot", + }, + "Pot Drop": { + cat: "General", + desc: "Decrease in Potential Rating", + sortSequence: ["desc", "asc"], + sortType: "number", + template: "attr", + attrs: ["potDrop"], + }, + Pick: { + attrs: ["draft"], + cat: "General", + desc: "Draft Pick", + sortType: "draftPick", + template: "Pick", + }, + Team: { + attrs: ["abbrev", "tid"], + cat: "General", + sortType: "string", + stats: ["abbrev", "tid"], + template: "Team", + }, + Weight: { + attrs: ["weight"], + cat: "General", + template: "Weight", + }, + Height: { + attrs: ["hgt"], + cat: "General", + template: "Height", + }, + InjuryType: { + cat: "General", + template: "InjuryType", + attrs: ["injury"], + desc: "Type of Injury", + title: "Type", + }, + InjuryLength: { + cat: "General", + template: "InjuryLength", + attrs: ["injury"], + title: "Games", + desc: "Number of Games", + sortSequence: ["desc", "asc"], + sortType: "number", + }, + "rating:endu": { + cat: "Rating", + desc: "Endurance", + ratings: ["endu"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Rating", + title: "End", + }, + "rating:hgt": { + cat: "Rating", + desc: "Height", + ratings: ["hgt"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Rating", + title: "Hgt", + }, + "rating:spd": { + cat: "Rating", + desc: "Speed", + ratings: ["spd"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Rating", + title: "Spd", + }, + "rating:stre": { + cat: "Rating", + desc: "Strength", + ratings: ["stre"], + sortSequence: ["desc", "asc"], + sortType: "number", + template: "Rating", + title: "Str", + }, + "stat:gp": { + cat: "Stat", + desc: "Games Played", + options: { decimals: 0 }, + sortSequence: ["desc", "asc"], + sortType: "number", + stats: ["gp"], + template: "Stat", + title: gp, + }, + "stat:gs": { + cat: "Stat", + desc: "Games Started", + options: { decimals: 0 }, + sortSequence: ["desc", "asc"], + sortType: "number", + stats: ["gs"], + template: "Stat", + title: "GS", + }, + "stat:jerseyNumber": { + cat: "General", + desc: "Jersey Number", + options: { decimals: 0 }, + sortSequence: ["asc", "desc"], + sortType: "number", + stats: ["jerseyNumber"], + template: "Stat", + title: "#", + }, + "stat:min": { + cat: "Stat", + desc: isSport("hockey") ? "Time On Ice" : "Minutes", + sortSequence: ["desc", "asc"], + sortType: "number", + stats: ["min"], + template: "Stat", + title: isSport("hockey") ? "TOI" : "MP", + }, + "stat:yearsWithTeam": { + cat: "General", + desc: "Years With Team", + sortSequence: ["desc", "asc"], + sortType: "number", + stats: ["yearsWithTeam"], + template: "Stat", + title: "YWT", + }, + ...sportSpecificCols, +}; + +const legacyCols: { + [key: string]: Partial; } = { "": { sortSequence: ["desc", "asc"], @@ -1987,21 +3212,7 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "number", }, - Acquired: { - desc: "How Player Was Acquired", - }, Actions: {}, - Age: { - sortType: "number", - }, - Amount: { - sortSequence: ["desc", "asc"], - sortType: "currency", - }, - "Asking For": { - sortSequence: ["desc", "asc"], - sortType: "currency", - }, "Avg Attendance": { sortSequence: ["desc", "asc"], sortType: "number", @@ -2035,17 +3246,11 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "number", }, - College: {}, Conference: {}, - Contract: { - sortSequence: ["desc", "asc"], - sortType: "currency", - }, Count: { sortSequence: ["desc", "asc"], sortType: "number", }, - Country: {}, Created: { desc: "Created Date", searchType: "string", @@ -2057,18 +3262,6 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "number", }, - "Current Contract": { - desc: "Current Contract", - sortSequence: ["desc", "asc"], - sortType: "currency", - title: "Current", - }, - "Projected Contract": { - desc: "Projected Contract", - sortSequence: ["desc", "asc"], - sortType: "currency", - title: "Projected", - }, Details: {}, Died: { sortSequence: ["desc", "asc"], @@ -2088,31 +3281,12 @@ const cols: { }, "Draft Picks": { sortSequence: [], - }, - "Draft Year": { - sortType: "number", - }, - Drafted: { - sortType: "number", + sortType: "draftPick", }, "Dunk Winner": { desc: "Slam Dunk Contest Winner", sortType: "name", }, - End: { - sortSequence: ["desc", "asc"], - sortType: "number", - }, - Exp: { - desc: "Contract Expiration", - sortSequence: ["asc", "desc"], - sortType: "number", - }, - Experience: { - desc: "Number of Years in the League", - sortSequence: ["desc", "asc"], - sortType: "number", - }, Finals: { desc: "Finals Appearances", sortSequence: ["desc", "asc"], @@ -2149,10 +3323,6 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "number", }, - Height: { - sortSequence: ["desc", "asc"], - sortType: "number", - }, HOF: { sortSequence: ["desc", "asc"], }, @@ -2195,11 +3365,6 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "number", }, - Mood: { - width: "1px", - sortSequence: ["desc", "asc"], - sortType: "number", - }, MVP: { desc: "Most Valuable Player", sortType: "name", @@ -2234,16 +3399,6 @@ const cols: { Opp: { desc: "Opponent", }, - Ovr: { - desc: "Overall Rating", - sortSequence: ["desc", "asc"], - sortType: "number", - }, - "Ovr Drop": { - desc: "Decrease in Overall Rating", - sortSequence: ["desc", "asc"], - sortType: "number", - }, PA: { desc: `${isSport("hockey") ? "Goals" : "Points"} Against`, sortSequence: ["desc", "asc"], @@ -2272,11 +3427,6 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "currency", }, - "Peak Ovr": { - desc: "Peak Overall Rating", - sortSequence: ["desc", "asc"], - sortType: "number", - }, Phase: { desc: "League Season and Phase", sortSequence: ["desc", "asc"], @@ -2295,19 +3445,6 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "number", }, - Pos: { - desc: "Position", - }, - Pot: { - desc: "Potential Rating", - sortSequence: ["desc", "asc"], - sortType: "number", - }, - "Pot Drop": { - desc: "Decrease in Potential Rating", - sortSequence: ["desc", "asc"], - sortType: "number", - }, "Pre-Lottery": { desc: "Pre-lottery rank", sortSequence: ["desc", "asc"], @@ -2322,16 +3459,6 @@ const cols: { sortSequence: ["desc", "asc"], sortType: "currency", }, - PTS: { - desc: "Points", - sortSequence: ["desc", "asc"], - sortType: "number", - }, - "PTS%": { - desc: "Points Divided By Maximum Points", - sortSequence: ["desc", "asc"], - sortType: "number", - }, Received: { desc: "Assets Received in Trade", }, @@ -2372,7 +3499,6 @@ const cols: { desc: "Playoff Seed", sortType: "number", }, - Skills: {}, Start: { sortSequence: ["desc", "asc"], sortType: "number", @@ -2409,82 +3535,15 @@ const cols: { Type: { desc: "Type of Game", }, - TypeInjury: { - desc: "Type of Injury", - title: "Type", - }, W: { desc: "Wins", sortSequence: ["desc", "asc"], sortType: "number", }, - Weight: { - sortSequence: ["desc", "asc"], - sortType: "number", - }, - X: { - desc: "Exclude from counter offers", - noSearch: true, - sortSequence: [], - }, Year: { sortType: "number", }, Summary: {}, - "rating:endu": { - desc: "Endurance", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "End", - }, - "rating:hgt": { - desc: "Height", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "Hgt", - }, - "rating:spd": { - desc: "Speed", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "Spd", - }, - "rating:stre": { - desc: "Strength", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "Str", - }, - "stat:gp": { - desc: "Games Played", - sortSequence: ["desc", "asc"], - sortType: "number", - title: gp, - }, - "stat:gpPerPlayer": { - desc: "Games Played Per Player", - sortSequence: ["desc", "asc"], - sortType: "number", - title: `${gp}/Player`, - }, - "stat:gs": { - desc: "Games Started", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "GS", - }, - "stat:jerseyNumber": { - desc: "Jersey Number", - sortSequence: ["asc", "desc"], - sortType: "number", - title: "#", - }, - "stat:min": { - desc: isSport("hockey") ? "Time On Ice" : "Minutes", - sortSequence: ["desc", "asc"], - sortType: "number", - title: isSport("hockey") ? "TOI" : "MP", - }, "stat:mov": { desc: "Average Margin of Victory", sortSequence: ["desc", "asc"], @@ -2497,12 +3556,6 @@ const cols: { sortType: "number", title: "Diff", }, - "stat:yearsWithTeam": { - desc: "Years With Team", - sortSequence: ["desc", "asc"], - sortType: "number", - title: "YWT", - }, "count:allDefense": { desc: "All-Defensive Team", sortSequence: ["desc", "asc"], @@ -2651,22 +3704,30 @@ const cols: { sortType: "number", title: "DROY", }, - ...sportSpecificCols, }; -export default ( - titles: string[], - overrides: Record> = {}, -): Col[] => { - return titles.map(title => { - if (!cols.hasOwnProperty(title)) { - throw new Error(`Unknown column: "${title}"`); - } - +export function getAllCols(): ColTemp[] { + return Object.entries(cols).map(([name, col]): Col => { return { - ...cols[title], - title: cols[title].title ?? title, - ...overrides[title], + ...col, + title: col.title ?? name, + key: name, }; }); +} + +export default ( + keys: string[], + overrides: Record> = {}, +): MetaCol[] => { + return keys.flatMap(key => { + const col: MetaCol = { + title: key, + ...(cols[key] ?? legacyCols[key] ?? {}), + ...overrides[key], + key: key, + }; + + return [col]; + }); }; diff --git a/src/ui/util/columns/getTemplate.ts b/src/ui/util/columns/getTemplate.ts new file mode 100644 index 0000000000..3ce1a0cf71 --- /dev/null +++ b/src/ui/util/columns/getTemplate.ts @@ -0,0 +1,14 @@ +import * as templates from "./templates"; +import type { MetaCol } from "./getCols"; +import type { Player } from "../../../common/types"; +import type { TableConfig } from "../TableConfig"; + +export default function (p: Player, c: MetaCol, config: TableConfig) { + if (c.template === undefined) return; + else if (typeof c.template === "function") + return c.template({ p, c, vars: config.vars }); + else if (!(c.template in templates)) return; + // @ts-ignore + // eslint-disable-next-line import/namespace + return templates[c.template]({ p, c, vars: config.vars }); +} diff --git a/src/ui/util/columns/templates/Age.tsx b/src/ui/util/columns/templates/Age.tsx new file mode 100644 index 0000000000..1530d4be14 --- /dev/null +++ b/src/ui/util/columns/templates/Age.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.age; diff --git a/src/ui/util/columns/templates/AskingFor.tsx b/src/ui/util/columns/templates/AskingFor.tsx new file mode 100644 index 0000000000..ebcf42d3b9 --- /dev/null +++ b/src/ui/util/columns/templates/AskingFor.tsx @@ -0,0 +1,5 @@ +import { helpers } from "../../index"; +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => + helpers.formatCurrency(p.mood.user.contractAmount / 1000, "M"); diff --git a/src/ui/util/columns/templates/Attr.tsx b/src/ui/util/columns/templates/Attr.tsx new file mode 100644 index 0000000000..20a79a896a --- /dev/null +++ b/src/ui/util/columns/templates/Attr.tsx @@ -0,0 +1,7 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => { + const key: string = c.attrs[0]; + if (!(key in p)) return `${key} not found`; + return p[key]; +}; diff --git a/src/ui/util/columns/templates/College.tsx b/src/ui/util/columns/templates/College.tsx new file mode 100644 index 0000000000..135a9e542a --- /dev/null +++ b/src/ui/util/columns/templates/College.tsx @@ -0,0 +1,18 @@ +import type { TemplateProps } from "../getCols"; +import { helpers } from "../../index"; + +export default ({ p, c, vars }: TemplateProps) => { + const college = p.college && p.college !== "" ? p.college : "None"; + return ( + + {college} + + ); +}; diff --git a/src/ui/util/columns/templates/Contract.tsx b/src/ui/util/columns/templates/Contract.tsx new file mode 100644 index 0000000000..435dd212bc --- /dev/null +++ b/src/ui/util/columns/templates/Contract.tsx @@ -0,0 +1,6 @@ +import { helpers } from "../../index"; +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => + helpers.formatCurrency(p.contract.amount, "M") + + (c.options?.format == "full" ? ` thru ${p.contract.exp}` : ""); diff --git a/src/ui/util/columns/templates/Country.tsx b/src/ui/util/columns/templates/Country.tsx new file mode 100644 index 0000000000..45a432e127 --- /dev/null +++ b/src/ui/util/columns/templates/Country.tsx @@ -0,0 +1,19 @@ +import type { TemplateProps } from "../getCols"; +import { helpers } from "../../index"; +import { CountryFlag } from "../../../components"; + +export default ({ p, c, vars }: TemplateProps) => ( + <> + + + {p.born.loc} + + +); diff --git a/src/ui/util/columns/templates/DraftYear.tsx b/src/ui/util/columns/templates/DraftYear.tsx new file mode 100644 index 0000000000..2b5fa1536b --- /dev/null +++ b/src/ui/util/columns/templates/DraftYear.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.draft.year; diff --git a/src/ui/util/columns/templates/Exp.tsx b/src/ui/util/columns/templates/Exp.tsx new file mode 100644 index 0000000000..ad83f66466 --- /dev/null +++ b/src/ui/util/columns/templates/Exp.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.contract.exp.toString(); diff --git a/src/ui/util/columns/templates/Experience.tsx b/src/ui/util/columns/templates/Experience.tsx new file mode 100644 index 0000000000..b7e8378bfb --- /dev/null +++ b/src/ui/util/columns/templates/Experience.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.experience.toString(); diff --git a/src/ui/util/columns/templates/Height.tsx b/src/ui/util/columns/templates/Height.tsx new file mode 100644 index 0000000000..0ecc412c2d --- /dev/null +++ b/src/ui/util/columns/templates/Height.tsx @@ -0,0 +1,4 @@ +import type { TemplateProps } from "../getCols"; +import { wrappedHeight } from "../../../components/Height"; + +export default ({ p, c, vars }: TemplateProps) => wrappedHeight(p.hgt); diff --git a/src/ui/util/columns/templates/InjuryLength.tsx b/src/ui/util/columns/templates/InjuryLength.tsx new file mode 100644 index 0000000000..1aa36adbb5 --- /dev/null +++ b/src/ui/util/columns/templates/InjuryLength.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.injury.gamesRemaining; diff --git a/src/ui/util/columns/templates/InjuryType.tsx b/src/ui/util/columns/templates/InjuryType.tsx new file mode 100644 index 0000000000..10c229636b --- /dev/null +++ b/src/ui/util/columns/templates/InjuryType.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.injury.type; diff --git a/src/ui/util/columns/templates/Mood.tsx b/src/ui/util/columns/templates/Mood.tsx new file mode 100644 index 0000000000..f3ecaa5f1e --- /dev/null +++ b/src/ui/util/columns/templates/Mood.tsx @@ -0,0 +1,5 @@ +import type { TemplateProps } from "../getCols"; +import { dataTableWrappedMood } from "../../../components/Mood"; + +export default ({ p, c, vars }: TemplateProps) => + dataTableWrappedMood({ defaultType: "user", maxWidth: true, p }); diff --git a/src/ui/util/columns/templates/MoodCurrent.tsx b/src/ui/util/columns/templates/MoodCurrent.tsx new file mode 100644 index 0000000000..c8feda1deb --- /dev/null +++ b/src/ui/util/columns/templates/MoodCurrent.tsx @@ -0,0 +1,5 @@ +import type { TemplateProps } from "../getCols"; +import { dataTableWrappedMood } from "../../../components/Mood"; + +export default ({ p, c, vars }: TemplateProps) => + dataTableWrappedMood({ defaultType: "current", maxWidth: true, p }); diff --git a/src/ui/util/columns/templates/Name.tsx b/src/ui/util/columns/templates/Name.tsx new file mode 100644 index 0000000000..d719b78b02 --- /dev/null +++ b/src/ui/util/columns/templates/Name.tsx @@ -0,0 +1,19 @@ +import type { TemplateProps } from "../getCols"; +import { PlayerNameLabels } from "../../../components"; + +export default ({ p, c, vars }: TemplateProps) => ( + + {p.name} + +); diff --git a/src/ui/util/columns/templates/Ovr.tsx b/src/ui/util/columns/templates/Ovr.tsx new file mode 100644 index 0000000000..a597d32293 --- /dev/null +++ b/src/ui/util/columns/templates/Ovr.tsx @@ -0,0 +1,13 @@ +import { RatingWithChange } from "../../../components"; +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => { + if (vars["challengeNoRatings"]) return ""; + else if (p.ratings["dovr"] && vars.phase === 0) + return ( + + {p.ratings["ovr"]} + + ); + else return p.ratings["ovr"]; +}; diff --git a/src/ui/util/columns/templates/Pick.tsx b/src/ui/util/columns/templates/Pick.tsx new file mode 100644 index 0000000000..c29c955892 --- /dev/null +++ b/src/ui/util/columns/templates/Pick.tsx @@ -0,0 +1,4 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => + p.draft.round > 0 ? `${p.draft.round}-${p.draft.pick}` : null; diff --git a/src/ui/util/columns/templates/Pos.tsx b/src/ui/util/columns/templates/Pos.tsx new file mode 100644 index 0000000000..d8586a78cb --- /dev/null +++ b/src/ui/util/columns/templates/Pos.tsx @@ -0,0 +1,3 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => p.ratings.pos; diff --git a/src/ui/util/columns/templates/Pot.tsx b/src/ui/util/columns/templates/Pot.tsx new file mode 100644 index 0000000000..6a938ccfb3 --- /dev/null +++ b/src/ui/util/columns/templates/Pot.tsx @@ -0,0 +1,13 @@ +import type { TemplateProps } from "../getCols"; +import { RatingWithChange } from "../../../components"; + +export default ({ p, c, vars }: TemplateProps) => { + if (vars["challengeNoRatings"]) return ""; + else if (p.ratings["dpot"] && vars.phase === 0) + return ( + + {p.ratings["pot"]} + + ); + else return p.ratings["pot"]; +}; diff --git a/src/ui/util/columns/templates/Projected.tsx b/src/ui/util/columns/templates/Projected.tsx new file mode 100644 index 0000000000..a1d37e9e54 --- /dev/null +++ b/src/ui/util/columns/templates/Projected.tsx @@ -0,0 +1,5 @@ +import type { TemplateProps } from "../getCols"; +import { helpers } from "../../index"; + +export default ({ p, c, vars }: TemplateProps) => + `${helpers.formatCurrency(p.contractDesired.amount, "M")}`; diff --git a/src/ui/util/columns/templates/Rating.tsx b/src/ui/util/columns/templates/Rating.tsx new file mode 100644 index 0000000000..c070617061 --- /dev/null +++ b/src/ui/util/columns/templates/Rating.tsx @@ -0,0 +1,7 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => { + if (vars["challengeNoRatings"]) return ""; + const key = c.ratings[0] ?? false; + return key && key in p.ratings ? p.ratings[key].toString() : ""; +}; diff --git a/src/ui/util/columns/templates/Stat.tsx b/src/ui/util/columns/templates/Stat.tsx new file mode 100644 index 0000000000..900f220dec --- /dev/null +++ b/src/ui/util/columns/templates/Stat.tsx @@ -0,0 +1,10 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => { + const key: string = c.stats[0]; + if (!(key in p.stats)) return `${key} not found`; + const value = p.stats[key]; + return typeof value == "number" + ? value.toFixed(c.options ? c.options.decimals : 1) + : value; +}; diff --git a/src/ui/util/columns/templates/Team.tsx b/src/ui/util/columns/templates/Team.tsx new file mode 100644 index 0000000000..c0614c2e27 --- /dev/null +++ b/src/ui/util/columns/templates/Team.tsx @@ -0,0 +1,10 @@ +import type { TemplateProps } from "../getCols"; +import { helpers } from "../../index"; + +export default ({ p, c, vars }: TemplateProps) => { + return ( + + {p.stats.abbrev} + + ); +}; diff --git a/src/ui/util/columns/templates/Weight.tsx b/src/ui/util/columns/templates/Weight.tsx new file mode 100644 index 0000000000..149f72fae0 --- /dev/null +++ b/src/ui/util/columns/templates/Weight.tsx @@ -0,0 +1,4 @@ +import type { TemplateProps } from "../getCols"; +import { wrappedWeight } from "../../../components/Weight"; + +export default ({ p, c, vars }: TemplateProps) => wrappedWeight(p.weight); diff --git a/src/ui/util/columns/templates/YearsWithTeam.tsx b/src/ui/util/columns/templates/YearsWithTeam.tsx new file mode 100644 index 0000000000..eb56d8061e --- /dev/null +++ b/src/ui/util/columns/templates/YearsWithTeam.tsx @@ -0,0 +1,5 @@ +import type { TemplateProps } from "../getCols"; + +export default ({ p, c, vars }: TemplateProps) => { + return p.stats.yearsWithTeam.toFixed(1); +}; diff --git a/src/ui/util/columns/templates/index.tsx b/src/ui/util/columns/templates/index.tsx new file mode 100644 index 0000000000..ec667304e7 --- /dev/null +++ b/src/ui/util/columns/templates/index.tsx @@ -0,0 +1,25 @@ +export { default as Age } from "./Age"; +export { default as AskingFor } from "./AskingFor"; +export { default as Attr } from "./Attr"; +export { default as Contract } from "./Contract"; +export { default as Country } from "./Country"; +export { default as College } from "./College"; +export { default as DraftYear } from "./DraftYear"; +export { default as Pick } from "./Pick"; +export { default as Exp } from "./Exp"; +export { default as Experience } from "./Experience"; +export { default as Height } from "./Height"; +export { default as InjuryLength } from "./InjuryLength"; +export { default as InjuryType } from "./InjuryType"; +export { default as Mood } from "./Mood"; +export { default as MoodCurrent } from "./MoodCurrent"; +export { default as Name } from "./Name"; +export { default as Ovr } from "./Ovr"; +export { default as Pos } from "./Pos"; +export { default as Pot } from "./Pot"; +export { default as Projected } from "./Projected"; +export { default as Rating } from "./Rating"; +export { default as Stat } from "./Stat"; +export { default as Team } from "./Team"; +export { default as Weight } from "./Weight"; +export { default as YearsWithTeam } from "./YearsWithTeam"; diff --git a/src/ui/util/index.ts b/src/ui/util/index.ts index 94a0eda494..eff39ccc98 100644 --- a/src/ui/util/index.ts +++ b/src/ui/util/index.ts @@ -32,7 +32,7 @@ export { default as confirm } from "./confirm"; export { default as confirmDeleteAllLeagues } from "./confirmDeleteAllLeagues"; export { default as downloadFile } from "./downloadFile"; export { default as genStaticPage } from "./genStaticPage"; -export { default as getCols } from "../../common/getCols"; +export { default as getCols } from "./columns/getCols"; export { default as getScript } from "./getScript"; export { default as gradientStyleFactory } from "./gradientStyleFactory"; export { default as groupAwards } from "./groupAwards"; diff --git a/src/ui/views/Achievements.tsx b/src/ui/views/Achievements.tsx index ccf579841d..671e7d4200 100644 --- a/src/ui/views/Achievements.tsx +++ b/src/ui/views/Achievements.tsx @@ -233,7 +233,7 @@ const Category = ({ > diff --git a/src/ui/views/AllStarHistory.tsx b/src/ui/views/AllStarHistory.tsx index 24ed689e77..739fe2175c 100644 --- a/src/ui/views/AllStarHistory.tsx +++ b/src/ui/views/AllStarHistory.tsx @@ -273,7 +273,7 @@ const AllStarHistory = ({ allAllStars, userTid }: View<"allStarHistory">) => { 0 ? ( ) => {
{rows.length > 0 ? ( - + <> +
+ + + ) : null} ); diff --git a/src/ui/views/Draft.tsx b/src/ui/views/Draft.tsx index c48ec7bb61..0e270273e8 100644 --- a/src/ui/views/Draft.tsx +++ b/src/ui/views/Draft.tsx @@ -432,7 +432,7 @@ const Draft = ({ 100} rows={rowsUndrafted} @@ -462,7 +462,7 @@ const Draft = ({ 100} rows={rowsDrafted} diff --git a/src/ui/views/DraftHistory.tsx b/src/ui/views/DraftHistory.tsx index 6f78e83709..6390023344 100644 --- a/src/ui/views/DraftHistory.tsx +++ b/src/ui/views/DraftHistory.tsx @@ -192,7 +192,7 @@ const DraftHistory = ({ diff --git a/src/ui/views/DraftTeamHistory.tsx b/src/ui/views/DraftTeamHistory.tsx index cd325ba1eb..df727a0164 100644 --- a/src/ui/views/DraftTeamHistory.tsx +++ b/src/ui/views/DraftTeamHistory.tsx @@ -182,7 +182,7 @@ const DraftTeamHistory = ({ ) => { - const [addFilters, setAddFilters] = useState< - (string | undefined)[] | undefined - >(); + const [addFilters, setAddFilters] = useState(); + + const config = TableConfig.unserialize(_config); + + config.addColumn( + { + key: "negotiate", + title: "Negotiate", + template: ({ p, c, vars }) => ( + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544 + // @ts-expect-error + + ), + }, + 1, + ); + + const cols = [...config.columns]; const showAfforablePlayers = useCallback(() => { - const newAddFilters: (string | undefined)[] = new Array(9 + stats.length); + let newAddFilters: Filter[]; if (capSpace * 1000 > minContract && !challengeNoFreeAgents) { - newAddFilters[newAddFilters.length - 3] = `<${capSpace}`; + newAddFilters = [{ col: "Asking For", value: `<${capSpace}` }]; } else { - newAddFilters[newAddFilters.length - 3] = `<${minContract / 1000}`; + newAddFilters = [{ col: "Asking For", value: `<${minContract / 1000}` }]; } setAddFilters(newAddFilters); @@ -48,7 +73,7 @@ const FreeAgents = ({ setTimeout(() => { setAddFilters(undefined); }, 0); - }, [capSpace, challengeNoFreeAgents, minContract, stats]); + }, [capSpace, challengeNoFreeAgents, minContract]); useTitleBar({ title: "Free Agents" }); @@ -73,61 +98,12 @@ const FreeAgents = ({ ); } - const cols = getCols([ - "Name", - "Pos", - "Age", - "Ovr", - "Pot", - ...stats.map(stat => `stat:${stat}`), - "Mood", - "Asking For", - "Exp", - "Negotiate", - ]); - const rows = players.map(p => { return { key: p.pid, - data: [ - - {p.name} - , - p.ratings.pos, - p.age, - !challengeNoRatings ? p.ratings.ovr : null, - !challengeNoRatings ? p.ratings.pot : null, - ...stats.map(stat => helpers.roundStat(p.stats[stat], stat)), - dataTableWrappedMood({ - defaultType: "user", - maxWidth: true, - p, - }), - helpers.formatCurrency(p.mood.user.contractAmount / 1000, "M"), - p.contract.exp, - { - value: ( - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/20544 - // @ts-expect-error - - ), - searchValue: p.mood.user.willing ? "Negotiate Sign" : "Refuses!", - }, - ], + data: Object.fromEntries( + cols.map(col => [col.key, getTemplate(p, col, config)]), + ), }; }); @@ -193,7 +169,8 @@ const FreeAgents = ({ ) => { { + ...teamsFiltered.map((t): LegacyCol => { return { classNames: classNames( "text-center", @@ -125,7 +125,7 @@ const HeadToHeadAll = ({ ) => { ) => { useTitleBar({ @@ -19,53 +18,12 @@ const Injuries = ({ dropdownFields: { teamsAndAllWatch: abbrev, seasonsAndCurrent: season }, }); - const cols = getCols([ - "Name", - "Pos", - "Team", - "Age", - "Ovr", - "Pot", - ...stats.map(stat => `stat:${stat}`), - "TypeInjury", - "Games", - "Ovr Drop", - "Pot Drop", - ]); - - const rows = injuries.map((p, i) => { - const showRatings = !challengeNoRatings || p.tid === PLAYER.RETIRED; - + const rows = injuries.map(p => { return { - key: season === "current" ? p.pid : i, - data: [ - - {p.name} - , - p.ratings.pos, - - {p.stats.abbrev} - , - p.age, - showRatings ? p.ratings.ovr : null, - showRatings ? p.ratings.pot : null, - ...stats.map(stat => helpers.roundStat(p.stats[stat], stat)), - p.type, - p.games, - showRatings ? p.ovrDrop : null, - showRatings ? p.potDrop : null, - ], + key: p.pid, + data: Object.fromEntries( + config.columns.map(col => [col.key, getTemplate(p, col, config)]), + ), classNames: { "table-danger": p.hof, "table-info": p.stats.tid === userTid, @@ -95,8 +53,9 @@ const Injuries = ({ {rows.length > 0 ? ( diff --git a/src/ui/views/Player/Injuries.tsx b/src/ui/views/Player/Injuries.tsx index fc27c023c2..217018621d 100644 --- a/src/ui/views/Player/Injuries.tsx +++ b/src/ui/views/Player/Injuries.tsx @@ -49,7 +49,7 @@ const Injuries = ({ { diff --git a/src/ui/views/Player/index.tsx b/src/ui/views/Player/index.tsx index 8c81067479..01639ee519 100644 --- a/src/ui/views/Player/index.tsx +++ b/src/ui/views/Player/index.tsx @@ -126,7 +126,7 @@ const StatsTable = ({ `rating:${rating}`), "Skills", ])} - defaultSort={[0, "asc"]} + defaultSort={["col1", "asc"]} hideAllControls name="Player:Ratings" rows={player.ratings.map((r, i) => { @@ -356,7 +356,7 @@ const Player2 = ({ ) => { useTitleBar({ @@ -24,120 +19,14 @@ const PlayerBios = ({ dropdownFields: { teamsAndAllWatch: abbrev, seasons: season }, }); - const cols = getCols([ - "Name", - "Pos", - "stat:jerseyNumber", - "Team", - "Age", - "Height", - "Weight", - "Mood", - "Contract", - "Exp", - "Country", - "College", - "Draft Year", - "Pick", - "Experience", - "Ovr", - "Pot", - ...stats.map(stat => `stat:${stat}`), - ]); + const cols = config.columns; const rows = players.map(p => { - const showRatings = !challengeNoRatings || p.tid === PLAYER.RETIRED; - const college = p.college && p.college !== "" ? p.college : "None"; - return { key: p.pid, - data: [ - - {p.name} - , - p.ratings.pos, - - {p.stats.jerseyNumber} - , - - {p.stats.abbrev} - , - p.age, - wrappedHeight(p.hgt), - wrappedWeight(p.weight), - dataTableWrappedMood({ - defaultType: - p.tid === PLAYER.FREE_AGENT || p.tid === PLAYER.UNDRAFTED - ? "user" - : "current", - maxWidth: true, - p, - }), - p.contract.amount > 0 - ? helpers.formatCurrency(p.contract.amount, "M") - : null, - p.contract.amount > 0 && season === currentSeason - ? p.contract.exp - : null, - { - value: ( - <> - - - {p.born.loc} - - - ), - sortValue: p.born.loc, - searchValue: p.born.loc, - }, - - {college} - , - p.draft.year, - p.draft.round > 0 ? `${p.draft.round}-${p.draft.pick}` : null, - p.experience, - showRatings ? p.ratings.ovr : null, - showRatings ? p.ratings.pot : null, - ...stats.map(stat => helpers.roundStat(p.stats[stat], stat)), - ], - classNames: { - "table-danger": p.hof, - "table-info": p.stats.tid === userTid, - }, + data: Object.fromEntries( + cols.map(col => [col.key, getTemplate(p, col, config)]), + ), }; }); @@ -152,7 +41,8 @@ const PlayerBios = ({ `stat:${stat}`), ]); - const makeRow = (game: typeof gameLog[number], i: number): DataTableRow => { + const makeRow = ( + game: typeof gameLog[number], + i: number, + ): LegacyDataTableRow => { return { key: i, data: [ @@ -213,7 +216,7 @@ const PlayerGameLog = ({ <> ) => { @@ -21,84 +21,19 @@ const PlayerRatings = ({ dropdownFields: { teamsAndAllWatch: abbrev, seasons: season }, }); - const ovrsPotsColNames: string[] = []; - if (isSport("football") || isSport("hockey")) { - for (const pos of POSITIONS) { - for (const type of ["ovr", "pot"]) { - ovrsPotsColNames.push(`rating:${type}${pos}`); - } - } - } - - const cols = getCols([ - "Name", - "Pos", - "Team", - "Age", - "Contract", - "Exp", - "Ovr", - "Pot", - ...ratings.map(rating => `rating:${rating}`), - ...ovrsPotsColNames, - ]); + const cols = config.columns; const rows = players.map(p => { - const showRatings = !challengeNoRatings || p.tid === PLAYER.RETIRED; - - const ovrsPotsRatings: string[] = []; - if (isSport("football") || isSport("hockey")) { - for (const pos of POSITIONS) { - for (const type of ["ovrs", "pots"]) { - ovrsPotsRatings.push(showRatings ? p.ratings[type][pos] : null); - } - } - } - return { key: p.pid, - data: [ - - {p.name} - , - p.ratings.pos, - - {p.stats.abbrev} - , - p.age, - p.contract.amount > 0 - ? helpers.formatCurrency(p.contract.amount, "M") - : null, - p.contract.amount > 0 && season === currentSeason - ? p.contract.exp - : null, - showRatings ? p.ratings.ovr : null, - showRatings ? p.ratings.pot : null, - ...ratings.map(rating => (showRatings ? p.ratings[rating] : null)), - ...ovrsPotsRatings, - ], - classNames: { - "table-danger": p.hof, - "table-info": p.stats.tid === userTid, - }, + data: Object.fromEntries( + cols.map(col => [col.key, getTemplate(p, col, config)]), + ), }; }); return ( - <> +
{challengeNoRatings ? ( @@ -109,7 +44,7 @@ const PlayerRatings = ({ ) : null}

- Players on your team are{" "} + Players on your team are highlighted in blue. Players in the Hall of Fame are highlighted in red . @@ -117,13 +52,23 @@ const PlayerRatings = ({ - +

); }; +PlayerRatings.propTypes = { + abbrev: PropTypes.string.isRequired, + currentSeason: PropTypes.number.isRequired, + players: PropTypes.arrayOf(PropTypes.object).isRequired, + config: PropTypes.object.isRequired, + season: PropTypes.number.isRequired, + userTid: PropTypes.number.isRequired, +}; + export default PlayerRatings; diff --git a/src/ui/views/PlayerStats.tsx b/src/ui/views/PlayerStats.tsx index 3ebea99545..6432f4b7d6 100644 --- a/src/ui/views/PlayerStats.tsx +++ b/src/ui/views/PlayerStats.tsx @@ -1,9 +1,10 @@ -import { DataTable, MoreLinks, PlayerNameLabels } from "../components"; +import PropTypes from "prop-types"; +import { DataTable, MoreLinks } from "../components"; import useTitleBar from "../hooks/useTitleBar"; -import { getCols, helpers } from "../util"; -import type { View } from "../../common/types"; +import { helpers } from "../util"; +import type { SortOrder, View } from "../../common/types"; import { isSport } from "../../common"; -import { wrappedAgeAtDeath } from "../components/AgeAtDeath"; +import getTemplate from "../util/columns/getTemplate"; export const formatStatGameHigh = ( ps: any, @@ -47,7 +48,7 @@ const PlayerStats = ({ playoffs, season, statType, - stats, + config, superCols, userTid, }: View<"playerStats">) => { @@ -64,112 +65,44 @@ const PlayerStats = ({ }, }); - const cols = getCols([ - "Name", - "Pos", - "Age", - "Team", - ...(season === "all" ? ["Season"] : []), - ...stats.map( - stat => `stat:${stat.endsWith("Max") ? stat.replace("Max", "") : stat}`, - ), - ]); + const cols = config.columns; - if (statType === "shotLocations") { - cols[cols.length - 7].title = "M"; - cols[cols.length - 6].title = "A"; - cols[cols.length - 5].title = "%"; - } - - let sortCol = cols.length - 1; + let sortCol: string = cols[0].key ?? "col1"; + let sortDir: SortOrder = "asc"; if (isSport("football")) { if (statType === "passing") { - sortCol = 9; + sortCol = "stat:passYds"; + sortDir = "desc"; } else if (statType === "rushing") { - sortCol = cols.length - 3; + sortCol = "stat:rusRecTD"; + sortDir = "desc"; } else if (statType === "defense") { - sortCol = 16; + sortCol = "stat:defSk"; + sortDir = "desc"; } else if (statType === "kicking") { - sortCol = cols.length - 11; + sortCol = "stat:fgPct"; + sortDir = "desc"; } else if (statType === "returns") { - sortCol = 12; + sortCol = "stat:krYds"; + sortDir = "desc"; } } const rows = players.map(p => { - let pos; - if (Array.isArray(p.ratings) && p.ratings.length > 0) { - pos = p.ratings.at(-1).pos; - } else if (p.ratings.pos) { - pos = p.ratings.pos; - } else { - pos = "?"; - } - - // HACKS to show right stats, info - let actualAbbrev; - let actualTid; if (season === "career") { p.stats = p.careerStats; - actualAbbrev = p.abbrev; - actualTid = p.tid; if (playoffs === "playoffs") { p.stats = p.careerStatsPlayoffs; } - } else { - actualAbbrev = p.stats.abbrev; - actualTid = p.stats.tid; } - - const statsRow = stats.map(stat => - formatStatGameHigh(p.stats, stat, statType), - ); - - const key = season === "all" ? `${p.pid}-${p.stats.season}` : p.pid; - return { - key, - data: [ - { - value: ( - - {p.nameAbbrev} - - ), - sortValue: p.name, - searchValue: p.name, - }, - pos, - - // Only show age at death for career totals, otherwise just use current age - season === "career" - ? wrappedAgeAtDeath(p.age, p.ageAtDeath) - : p.stats.season - p.born.year, - - - {actualAbbrev} - , - - ...(season === "all" ? [p.stats.season] : []), - - ...statsRow, - ], + key: season === "all" ? `${p.pid}-${p.stats.season}` : p.pid, + data: Object.fromEntries( + cols.map(col => [col.key, getTemplate(p, col, config)]), + ), classNames: { "table-danger": p.hof, - "table-info": actualTid === userTid, + "table-info": p.stats.tid === userTid || p.tid === userTid, }, }; }); @@ -193,7 +126,8 @@ const PlayerStats = ({ 100} rows={rows} diff --git a/src/ui/views/ScheduledEvents.tsx b/src/ui/views/ScheduledEvents.tsx index ba2e600c70..dbacbe2e24 100644 --- a/src/ui/views/ScheduledEvents.tsx +++ b/src/ui/views/ScheduledEvents.tsx @@ -338,7 +338,7 @@ const ScheduledEvents = ({ scheduledEvents }: View<"scheduledEvents">) => { diff --git a/src/ui/views/TeamFinances.tsx b/src/ui/views/TeamFinances.tsx index 6ba9c4e4ae..3052e4899b 100644 --- a/src/ui/views/TeamFinances.tsx +++ b/src/ui/views/TeamFinances.tsx @@ -780,7 +780,7 @@ const TeamFinances = ({ ; -type Stats = TradeProps["stats"]; type Picks = TradeProps["userRoster"]; type Roster = TradeProps["otherRoster"]; const genPlayerRows = ( players: Roster, - handleToggle: HandleToggle, - userOrOther: UserOrOther, - stats: Stats, - challengeNoRatings: boolean, + playerCols: MetaCol[], + config: TableConfig, ) => { return players.map(p => { return { key: p.pid, - data: [ - { - handleToggle(userOrOther, "player", "include", p.pid); - }} - />, - { - handleToggle(userOrOther, "player", "exclude", p.pid); - }} - />, - - {p.name} - , - p.ratings.pos, - p.age, - !challengeNoRatings ? p.ratings.ovr : null, - !challengeNoRatings ? p.ratings.pot : null, - helpers.formatCurrency(p.contract.amount, "M"), - p.contract.exp, - ...stats.map(stat => helpers.roundStat(p.stats[stat], stat)), - ], + data: Object.fromEntries( + playerCols.map(col => [col.key, getTemplate(p, col, config)]), + ), classNames: { "table-danger": (p.excluded || p.untradable) && !p.included, "table-success": p.included, @@ -124,55 +91,67 @@ const pickCols = getCols(["", "X", "Draft Picks"], { }); const AssetList = ({ - challengeNoRatings, + config, handleBulk, handleToggle, numDraftRounds, picks, roster, - stats, userOrOther, }: { - challengeNoRatings: boolean; + config: TableConfig; handleBulk: HandleBulk; handleToggle: HandleToggle; numDraftRounds: number; picks: Picks; roster: Roster; - stats: Stats; userOrOther: UserOrOther; }) => { - const playerCols = getCols( - [ - "", - "X", - "Name", - "Pos", - "Age", - "Ovr", - "Pot", - "Contract", - "Exp", - ...stats.map(stat => `stat:${stat}`), - ], + config.updateColumn({ width: "100%" }, "Name"); + config.addColumn( { - "": { - sortSequence: [], - noSearch: true, - }, - Name: { - width: "100%", - }, + title: "", + key: "include", + sortSequence: [], + noSearch: true, + template: ({ p, c, vars }) => ( + { + handleToggle(userOrOther, "player", "include", p.pid); + }} + /> + ), }, + 0, ); - - const playerRows = genPlayerRows( - roster, - handleToggle, - userOrOther, - stats, - challengeNoRatings, + config.addColumn( + { + title: "X", + key: "exclude", + sortSequence: [], + noSearch: true, + template: ({ p, c, vars }) => ( + { + handleToggle(userOrOther, "player", "exclude", p.pid); + }} + /> + ), + }, + 1, ); + const playerCols = [...config.columns]; + + const playerRows = genPlayerRows(roster, playerCols, config); + const pickRows = genPickRows(picks, handleToggle, userOrOther); const userOrOtherKey = `${userOrOther[0].toUpperCase()}${userOrOther.slice( @@ -210,7 +189,8 @@ const AssetList = ({ @@ -253,7 +233,7 @@ const AssetList = ({ ) => { const [state, setState] = useState({ @@ -222,8 +223,7 @@ const Trade = (props: View<"trade">) => { }; const { - challengeNoRatings, - challengeNoTrades, + config: _config, gameOver, otherTeamsWantToHire, godMode, @@ -240,7 +240,6 @@ const Trade = (props: View<"trade">) => { salaryCapType, summary, showResigningMsg, - stats, strategy, teams, tied, @@ -296,8 +295,8 @@ const Trade = (props: View<"trade">) => { phase === PHASE.EXPANSION_DRAFT || gameOver || otherTeamsWantToHire || - spectator || - challengeNoTrades; + _config.vars.spectator || + _config.vars.challengeNoTrades; const numAssets = summary.teams[0].picks.length + @@ -313,6 +312,8 @@ const Trade = (props: View<"trade">) => { : "Other team"; const teamNames = [otherTeamName, userTeamName] as [string, string]; + const config = TableConfig.unserialize(_config); + return ( <>
@@ -377,25 +378,23 @@ const Trade = (props: View<"trade">) => {

{userTeamName}

@@ -472,7 +471,7 @@ const Trade = (props: View<"trade">) => { teamNames={teamNames} /> - ) : challengeNoTrades ? ( + ) : _config.vars.challengeNoTrades ? (

Challenge Mode: You're not allowed to make trades.

diff --git a/src/ui/views/TradingBlock.tsx b/src/ui/views/TradingBlock.tsx index b7ab4e0ab3..1891de09e1 100644 --- a/src/ui/views/TradingBlock.tsx +++ b/src/ui/views/TradingBlock.tsx @@ -1,10 +1,13 @@ -import { useRef, useState, ReactNode } from "react"; +import PropTypes from "prop-types"; +import { ReactNode, useRef, useState } from "react"; import { PHASE } from "../../common"; import useTitleBar from "../hooks/useTitleBar"; import { getCols, helpers, toWorker } from "../util"; -import { DataTable, PlayerNameLabels } from "../components"; +import { DataTable } from "../components"; import type { View } from "../../common/types"; import type api from "../../worker/api"; +import getTemplate from "../util/columns/getTemplate"; +import { TableConfig } from "../util/TableConfig"; type OfferType = Awaited< ReturnType @@ -18,13 +21,12 @@ type OfferProps = { otherDpids: number[], ) => Promise; i: number; - stats: string[]; + config: TableConfig; } & OfferType; const Offer = (props: OfferProps) => { const { abbrev, - challengeNoRatings, dpids, handleClickNegotiate, i, @@ -36,7 +38,7 @@ const Offer = (props: OfferProps) => { pids, players, region, - stats, + config, strategy, tid, tied, @@ -46,38 +48,14 @@ const Offer = (props: OfferProps) => { let offerPlayers: ReactNode = null; if (players.length > 0) { - const cols = getCols([ - "Name", - "Pos", - "Age", - "Ovr", - "Pot", - "Contract", - "Exp", - ...stats.map(stat => `stat:${stat}`), - ]); + const cols = config.columns; const rows = players.map(p => { return { key: p.pid, - data: [ - - {p.name} - , - p.ratings.pos, - p.age, - !challengeNoRatings ? p.ratings.ovr : null, - !challengeNoRatings ? p.ratings.pot : null, - helpers.formatCurrency(p.contract.amount, "M"), - p.contract.exp, - ...stats.map(stat => helpers.roundStat(p.stats[stat], stat)), - ], + data: Object.fromEntries( + cols.map(col => [col.key, getTemplate(p, col, config)]), + ), }; }); @@ -85,7 +63,8 @@ const Offer = (props: OfferProps) => {
{ ); }; +Offer.propTypes = { + abbrev: PropTypes.string.isRequired, + dpids: PropTypes.arrayOf(PropTypes.number).isRequired, + handleClickNegotiate: PropTypes.func.isRequired, + i: PropTypes.number.isRequired, + lost: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + payroll: PropTypes.number.isRequired, + picks: PropTypes.arrayOf(PropTypes.object).isRequired, + pids: PropTypes.arrayOf(PropTypes.number).isRequired, + players: PropTypes.arrayOf(PropTypes.object).isRequired, + region: PropTypes.string.isRequired, + config: PropTypes.object.isRequired, + strategy: PropTypes.string.isRequired, + tid: PropTypes.number.isRequired, + tied: PropTypes.number, + warning: PropTypes.string, + won: PropTypes.number.isRequired, +}; + const pickCols = getCols(["", "Draft Picks"], { "": { sortSequence: [], @@ -243,7 +242,7 @@ const TradingBlock = (props: View<"tradingBlock">) => { gameOver, spectator, phase, - stats, + config: _config, userPicks, userRoster, } = props; @@ -283,54 +282,35 @@ const TradingBlock = (props: View<"tradingBlock">) => { ); } - const cols = getCols( - [ - "", - "Name", - "Pos", - "Age", - "Ovr", - "Pot", - "Contract", - "Exp", - ...stats.map(stat => `stat:${stat}`), - ], - { - "": { - sortSequence: [], - noSearch: true, - }, - }, - ); + const config = TableConfig.unserialize(_config); - const rows = userRoster.map(p => { - return { - key: p.pid, - data: [ + config.addColumn( + { + title: "", + key: "include", + sortSequence: [], + noSearch: true, + template: ({ p, c, vars }) => ( handleChangeAsset("pids", p.pid)} title={p.untradableMsg} - />, - - {p.name} - , - p.ratings.pos, - p.age, - !challengeNoRatings ? p.ratings.ovr : null, - !challengeNoRatings ? p.ratings.pot : null, - helpers.formatCurrency(p.contract.amount, "M"), - p.contract.exp, - ...stats.map(stat => helpers.roundStat(p.stats[stat], stat)), - ], + /> + ), + }, + 0, + ); + + const cols = [...config.columns]; + + const rows = userRoster.map(p => { + return { + key: p.pid, + data: Object.fromEntries( + cols.map(col => [col.key, getTemplate(p, col, config)]), + ), }; }); @@ -362,7 +342,8 @@ const TradingBlock = (props: View<"tradingBlock">) => {
@@ -370,7 +351,6 @@ const TradingBlock = (props: View<"tradingBlock">) => {
) => { return ( ); @@ -419,4 +399,11 @@ const TradingBlock = (props: View<"tradingBlock">) => { ); }; +TradingBlock.propTypes = { + gameOver: PropTypes.bool.isRequired, + phase: PropTypes.number.isRequired, + userPicks: PropTypes.arrayOf(PropTypes.object).isRequired, + userRoster: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + export default TradingBlock; diff --git a/src/ui/views/TragicDeaths.tsx b/src/ui/views/TragicDeaths.tsx index d590e39cdf..01644702bf 100644 --- a/src/ui/views/TragicDeaths.tsx +++ b/src/ui/views/TragicDeaths.tsx @@ -3,6 +3,7 @@ import { getCols, helpers } from "../util"; import { DataTable, SafeHtml } from "../components"; import type { View } from "../../common/types"; import { frivolitiesMenu } from "./Frivolities"; +import type { LegacyDataTableRow } from "../components/DataTable"; const TragicDeaths = ({ players, stats, userTid }: View<"tragicDeaths">) => { useTitleBar({ title: "Tragic Deaths", customMenu: frivolitiesMenu }); @@ -45,7 +46,7 @@ const TragicDeaths = ({ players, stats, userTid }: View<"tragicDeaths">) => { "Details", ]); - const rows = players.map((p, i) => { + const rows: LegacyDataTableRow[] = players.map((p, i) => { const lastRatings = p.ratings.at(-1); const lastStats = p.stats.at(-1); @@ -97,7 +98,7 @@ const TragicDeaths = ({ players, stats, userTid }: View<"tragicDeaths">) => { `stat:${s}`), + ]; + console.log(fallback); + const columns: MetaCol[] = getCols(colOptions ?? fallback); + const statsNeeded: string[] = uniq( + columns.reduce( + (needed: string[], c: MetaCol) => needed.concat(c.stats ?? []), + [], + ), + ); + const ratingsNeeded: string[] = uniq( + columns.reduce( + (needed: string[], c: MetaCol) => needed.concat(c.ratings ?? []), + [], + ), + ); + const attrsNeeded: string[] = uniq( + columns.reduce( + (needed: string[], c: MetaCol) => needed.concat(c.attrs ?? []), + [], + ), + ); + // const vars = { + // season: g.get("season"), + // userTid: g.get("userTid"), + // godMode: g.get("godMode"), + // spectator: g.get("spectator"), + // phase: g.get("phase"), + // challengeNoRatings: g.get("challengeNoRatings"), + // challengeNoDraftPicks: g.get("challengeNoDraftPicks"), + // challengeNoFreeAgents: g.get("challengeNoFreeAgents"), + // challengeNoTrades: g.get("challengeNoTrades"), + // salaryCapType: g.get("salaryCapType"), + // salaryCap: g.get("salaryCap"), + // maxContract: g.get("maxContract"), + // minContract: g.get("minContract"), + // }; + + return getPlayers(inputs, statsNeeded, ratingsNeeded, attrsNeeded); +} + +export const getPlayers = async ( + inputs: PlayerTableInput, + statsNeeded: string[], + ratingsNeeded: string[], + attrsNeeded: string[], +) => { + let playersAll; + let tid: number | undefined = g + .get("teamInfoCache") + .findIndex(t => t.abbrev === inputs.teamsAndAllWatch); + if (tid < 0) { + tid = undefined; + } + + if (g.get("season") === inputs.season) { + playersAll = await idb.cache.players.indexGetAll("playersByTid", [ + PLAYER.FREE_AGENT, + Infinity, + ]); + } else { + playersAll = await idb.getCopies.players( + { + activeSeason: + typeof inputs.season === "number" ? inputs.season : undefined, + }, + "noCopyCache", + ); + } + // Show all teams + if (tid === undefined && inputs.teamsAndAllWatch === "watch") { + playersAll = playersAll.filter(p => p.watch); + } + + return playersAll; + + // Show all teams + let statType: PlayerStatType; + + if (isSport("basketball")) { + if (inputs.statType === "totals") { + statType = "totals"; + } else if (inputs.statType === "per36") { + statType = "per36"; + } else { + statType = "perGame"; + } + } else { + statType = "totals"; + } + + // idb.getCopies.playersPlus `tid` option doesn't work well enough (factoring in showNoStats and showRookies), so let's do it manually + // For the current season, use the current abbrev (including FA), not the last stats abbrev + // For other seasons, use the stats abbrev for filtering + let players = await idb.getCopies.playersPlus(playersAll, { + attrs: [], + ratings: [], + stats: [], + season: typeof inputs.season === "number" ? inputs.season : undefined, + tid, + statType, + playoffs: inputs.playoffs === "playoffs", + regularSeason: inputs.playoffs !== "playoffs", + mergeStats: true, + }); + + if (inputs.season === "all") { + players = players + .map(p => + p.stats.map((ps: any) => { + const ratings = + p.ratings.find((pr: any) => pr.season === ps.season) ?? + p.ratings.at(-1); + + return { + ...p, + ratings, + stats: ps, + }; + }), + ) + .flat(); + } + + // Only keep players who actually played + if (inputs.abbrev !== "watch" && isSport("basketball")) { + players = players.filter(p => { + if (inputs.statType === "gameHighs") { + if (inputs.season !== "career") { + return p.stats.gp > 0; + } else if (inputs.playoffs !== "playoffs") { + return p.careerStats.gp > 0; + } + return p.careerStatsPlayoffs.gp > 0; + } + + if (inputs.season !== "career") { + return p.stats.gp > 0; + } else if (inputs.playoffs === "playoffs") { + return p.careerStatsPlayoffs.gp > 0; + } else if (inputs.playoffs !== "playoffs") { + return p.careerStats.gp > 0; + } + + return false; + }); + } else if ( + inputs.abbrev !== "watch" && + statsTable.onlyShowIf && + (isSport("football") || isSport("hockey")) + ) { + // Ensure some non-zero stat for this position + const onlyShowIf = statsTable.onlyShowIf; + + let obj: "careerStatsPlayoffs" | "careerStats" | "stats"; + if (inputs.season === "career") { + if (inputs.playoffs === "playoffs") { + obj = "careerStatsPlayoffs"; + } else { + obj = "careerStats"; + } + } else { + obj = "stats"; + } + + players = players.filter(p => { + for (const stat of onlyShowIf) { + if (typeof p[obj][stat] === "number" && p[obj][stat] > 0) { + return true; + } + } + + return false; + }); + } + + return players; +}; diff --git a/src/worker/api/index.ts b/src/worker/api/index.ts index 1fcdad8a37..d8b19ba0e1 100644 --- a/src/worker/api/index.ts +++ b/src/worker/api/index.ts @@ -112,9 +112,11 @@ import type { PlayerRatings } from "../../common/types.basketball"; import createStreamFromLeagueObject from "../core/league/create/createStreamFromLeagueObject"; import type { IDBPIndex, IDBPObjectStore } from "idb"; import type { LeagueDB } from "../db/connectLeague"; +import { TableConfig } from "../../ui/util/TableConfig"; import playMenu from "./playMenu"; import toolsMenu from "./toolsMenu"; import omit from "lodash-es/omit"; +import getPlayerTable from "./getPlayerTable"; const acceptContractNegotiation = async ({ pid, @@ -1607,10 +1609,28 @@ const getTradingBlockOffers = async ({ "noCopyCache", ); const stats = bySport({ - basketball: ["gp", "min", "pts", "trb", "ast", "per"], - football: ["gp", "keyStats", "av"], - hockey: ["gp", "keyStats", "ops", "dps", "ps"], + basketball: [ + "stat:gp", + "stat:min", + "stat:pts", + "stat:trb", + "stat:ast", + "stat:per", + ], + football: ["stat:gp", "stat:keyStats", "stat:av"], + hockey: ["stat:gp", "stat:keyStats", "stat:ops", "stat:dps", "stat:ps"], }); + const config: TableConfig = new TableConfig("tradingBlock", [ + "Name", + "Pos", + "Age", + "Ovr", + "Pot", + "Contract", + "Exp", + ...stats, + ]); + await config.load(); // Take the pids and dpids in each offer and get the info needed to display the offer return Promise.all( @@ -1627,17 +1647,9 @@ const getTradingBlockOffers = async ({ ); playersAll = playersAll.filter(p => offer.pids.includes(p.pid)); const players = await idb.getCopies.playersPlus(playersAll, { - attrs: [ - "pid", - "name", - "age", - "contract", - "injury", - "watch", - "jerseyNumber", - ], - ratings: ["ovr", "pot", "skills", "pos"], - stats, + attrs: config.attrsNeeded, + ratings: config.ratingsNeeded, + stats: config.statsNeeded, season: g.get("season"), tid, showNoStats: true, @@ -3145,6 +3157,10 @@ const updateMultiTeamMode = async (gameAttributes: { await toUI("realtimeUpdate", [["gameAttributes"]]); }; +const updateColumns = async (data: { key: string; columns: string[] }) => { + await idb.meta.put("tables", data.columns, data.key); +}; + const updateOptions = async ( options: Options & { realPlayerPhotos: string; @@ -3876,6 +3892,7 @@ export default { getPlayersCommandPalette, getLocal, getPlayerBioInfoDefaults, + getPlayerTable, getPlayerWatch, getRandomCollege, getRandomCountry, @@ -3935,6 +3952,7 @@ export default { updateLeague, updateMultiTeamMode, updateOptions, + updateColumns, updatePlayThroughInjuries, updatePlayerWatch, updatePlayingTime, diff --git a/src/worker/db/connectMeta.ts b/src/worker/db/connectMeta.ts index e5cd368697..e5b5054f8b 100644 --- a/src/worker/db/connectMeta.ts +++ b/src/worker/db/connectMeta.ts @@ -34,6 +34,10 @@ export interface MetaDB extends DBSchema { | "realTeamInfo" | "defaultSettingsOverrides"; }; + tables: { + key: string; + value: string[]; + }; leagues: { value: League; key: number; @@ -42,6 +46,7 @@ export interface MetaDB extends DBSchema { } const create = (db: IDBPDatabase) => { + db.createObjectStore("tables"); db.createObjectStore("achievements", { keyPath: "aid", autoIncrement: true, @@ -81,6 +86,9 @@ const migrate = async ({ } // New ones here! + if (oldVersion <= 8) { + db.createObjectStore("tables"); + } // In next version, can do: // attributeStore.delete("lastSelectedTid"); diff --git a/src/worker/views/freeAgents.ts b/src/worker/views/freeAgents.ts index 25a464f236..b6405d1a59 100644 --- a/src/worker/views/freeAgents.ts +++ b/src/worker/views/freeAgents.ts @@ -3,6 +3,7 @@ import type { Player } from "../../common/types"; import { player, team } from "../core"; import { idb } from "../db"; import { g } from "../util"; +import { TableConfig } from "../../ui/util/TableConfig"; export const addMood = async (players: Player[]) => { const moods: Awaited>[] = []; @@ -27,25 +28,37 @@ const updateFreeAgents = async () => { await idb.cache.players.indexGetAll("playersByTid", PLAYER.FREE_AGENT), ); const capSpace = (g.get("salaryCap") - payroll) / 1000; + const stats = bySport({ - basketball: ["min", "pts", "trb", "ast", "per"], - football: ["gp", "keyStats", "av"], - hockey: ["gp", "keyStats", "ops", "dps", "ps"], + basketball: [ + "stat:gp", + "stat:min", + "stat:pts", + "stat:trb", + "stat:ast", + "stat:per", + ], + football: ["stat:gp", "stat:keyStats", "stat:av"], + hockey: ["stat:gp", "stat:keyStats", "stat:ops", "stat:dps", "stat:ps"], }); + const config: TableConfig = new TableConfig("freeAgents", [ + "Name", + "Pos", + "Age", + "Ovr", + "Pot", + ...stats, + "Mood", + "Asking For", + "Exp", + ]); + await config.load(); + const players = await idb.getCopies.playersPlus(playersAll, { - attrs: [ - "pid", - "name", - "age", - "contract", - "injury", - "watch", - "jerseyNumber", - "mood", - ], - ratings: ["ovr", "pot", "skills", "pos"], - stats, + attrs: config.attrsNeeded, + ratings: config.ratingsNeeded, + stats: config.statsNeeded, season: g.get("season"), showNoStats: true, showRookies: true, @@ -73,8 +86,8 @@ const updateFreeAgents = async () => { numRosterSpots: g.get("maxRosterSize") - userPlayers.length, spectator: g.get("spectator"), phase: g.get("phase"), + config, players, - stats, userPlayers, }; }; diff --git a/src/worker/views/injuries.ts b/src/worker/views/injuries.ts index db8b0c11bc..86e1d6626a 100644 --- a/src/worker/views/injuries.ts +++ b/src/worker/views/injuries.ts @@ -2,6 +2,7 @@ import { bySport, PHASE } from "../../common"; import { g } from "../util"; import type { UpdateEvents, ViewInput } from "../../common/types"; import { getPlayers } from "./playerRatings"; +import { TableConfig } from "../../ui/util/TableConfig"; const updateInjuries = async ( inputs: ViewInput<"injuries">, @@ -22,6 +23,20 @@ const updateInjuries = async ( hockey: ["gp", "keyStats"], }); + const config = new TableConfig("injuries", [ + "Name", + "Pos", + "Team", + "Age", + "Ovr", + "Pot", + ...stats.map(stat => `stat:${stat}`), + "TypeInjury", + "Games", + "Ovr Drop", + "Pot Drop", + ]); + const players = await getPlayers( inputs.season === "current" ? g.get("season") : inputs.season, inputs.abbrev, @@ -29,6 +44,7 @@ const updateInjuries = async ( ["ovr", "pot"], [...stats, "jerseyNumber"], inputs.tid, + config, ); const injuries = []; @@ -67,7 +83,7 @@ const updateInjuries = async ( godMode: g.get("godMode"), injuries, season: inputs.season, - stats, + config, userTid, }; } diff --git a/src/worker/views/playerBios.ts b/src/worker/views/playerBios.ts index 204b3755b1..72bc2349b0 100644 --- a/src/worker/views/playerBios.ts +++ b/src/worker/views/playerBios.ts @@ -4,6 +4,7 @@ import type { UpdateEvents, ViewInput } from "../../common/types"; import { getPlayers } from "./playerRatings"; import { player } from "../core"; import { idb } from "../db"; +import { TableConfig } from "../../ui/util/TableConfig"; const updatePlayers = async ( inputs: ViewInput<"playerBios">, @@ -11,6 +12,7 @@ const updatePlayers = async ( state: any, ) => { if ( + updateEvents.includes("customizeTable") || (inputs.season === g.get("season") && (updateEvents.includes("gameSim") || updateEvents.includes("playerMovement"))) || @@ -24,15 +26,37 @@ const updatePlayers = async ( hockey: ["keyStats"], }); + const config: TableConfig = new TableConfig("playerBios", [ + "Name", + "Pos", + "stat:jerseyNumber", + "Team", + "Age", + "Height", + "Weight", + "Mood", + "Contract", + "Exp", + "Country", + "College", + "DraftYear", + "Pick", + "Experience", + "Ovr", + "Pot", + ...stats.map(s => `stat:${s}`), + ]); + await config.load(); + const players = await getPlayers( inputs.season, inputs.abbrev, - ["born", "college", "hgt", "weight", "draft", "experience"], - ["ovr", "pot"], - [...stats, "jerseyNumber"], + config.attrsNeeded, + config.ratingsNeeded, + config.statsNeeded, inputs.tid, + config, ); - const userTid = g.get("userTid"); for (const p of players) { @@ -46,11 +70,10 @@ const updatePlayers = async ( return { abbrev: inputs.abbrev, - challengeNoRatings: g.get("challengeNoRatings"), + config: config, currentSeason: g.get("season"), season: inputs.season, players, - stats, userTid, }; } diff --git a/src/worker/views/playerRatings.ts b/src/worker/views/playerRatings.ts index 980a7fb6f2..b72cc996c4 100644 --- a/src/worker/views/playerRatings.ts +++ b/src/worker/views/playerRatings.ts @@ -1,7 +1,8 @@ -import { bySport, PHASE, PLAYER } from "../../common"; +import { bySport, isSport, PHASE, PLAYER, POSITIONS } from "../../common"; import { idb } from "../db"; import { g } from "../util"; import type { UpdateEvents, ViewInput } from "../../common/types"; +import { TableConfig } from "../../ui/util/TableConfig"; export const getPlayers = async ( season: number, @@ -10,6 +11,7 @@ export const getPlayers = async ( ratings: string[], stats: string[], tid: number | undefined, + config: TableConfig, ) => { let playersAll; @@ -31,20 +33,9 @@ export const getPlayers = async ( } let players = await idb.getCopies.playersPlus(playersAll, { - attrs: [ - "pid", - "name", - "age", - "contract", - "injury", - "hof", - "watch", - "tid", - "abbrev", - ...attrs, - ], - ratings: ["ovr", "pot", "skills", "pos", ...ratings], - stats: ["abbrev", "tid", "jerseyNumber", ...stats], + attrs: config.attrsNeeded, + ratings: config.ratingsNeeded, + stats: config.statsNeeded, season: season, showNoStats: true, showRookies: true, @@ -76,6 +67,7 @@ const updatePlayers = async ( state: any, ) => { if ( + updateEvents.includes("customizeTable") || (inputs.season === g.get("season") && updateEvents.includes("playerMovement")) || (updateEvents.includes("newPhase") && g.get("phase") === PHASE.PRESEASON) || @@ -146,6 +138,29 @@ const updatePlayers = async ( hockey: ["ovrs", "pots"], }); + const ovrsPotsColNames: string[] = []; + if (isSport("football") || isSport("hockey")) { + for (const pos of POSITIONS) { + for (const type of ["ovr", "pot"]) { + ovrsPotsColNames.push(`rating:${type}${pos}`); + } + } + } + + const config: TableConfig = new TableConfig("playerRatings", [ + "Name", + "Pos", + "Team", + "Age", + "Contract", + "Exp", + "Ovr", + "Pot", + ...ratings.map(rating => `rating:${rating}`), + ...ovrsPotsColNames, + ]); + await config.load(); + const players = await getPlayers( inputs.season, inputs.abbrev, @@ -153,6 +168,7 @@ const updatePlayers = async ( [...ratings, ...extraRatings], [], inputs.tid, + config, ); return { @@ -161,7 +177,7 @@ const updatePlayers = async ( currentSeason: g.get("season"), season: inputs.season, players, - ratings, + config, userTid: g.get("userTid"), }; } diff --git a/src/worker/views/playerStats.ts b/src/worker/views/playerStats.ts index 8feaea2018..e3a4c8bc59 100644 --- a/src/worker/views/playerStats.ts +++ b/src/worker/views/playerStats.ts @@ -6,6 +6,7 @@ import type { ViewInput, PlayerStatType, } from "../../common/types"; +import { TableConfig } from "../../ui/util/TableConfig"; const updatePlayers = async ( inputs: ViewInput<"playerStats">, @@ -13,6 +14,7 @@ const updatePlayers = async ( state: any, ) => { if ( + updateEvents.includes("customizeTable") || (inputs.season === g.get("season") && (updateEvents.includes("gameSim") || updateEvents.includes("playerMovement"))) || @@ -42,7 +44,6 @@ const updatePlayers = async ( throw new Error(`Invalid statType: "${inputs.statType}"`); } - const stats = statsTable.stats; let playersAll; if (g.get("season") === inputs.season && g.get("phase") <= PHASE.PLAYOFFS) { @@ -87,22 +88,18 @@ const updatePlayers = async ( playersAll = playersAll.filter(p => p.watch); } + const config: TableConfig = new TableConfig( + "playerStats." + inputs.statType, + ["Name", "Pos", "Team", "Age", ...statsTable.stats.map(s => `stat:${s}`)], + ); + await config.load(); + + config.setVar("season", inputs.season); + let players = await idb.getCopies.playersPlus(playersAll, { - attrs: [ - "pid", - "nameAbbrev", - "name", - "age", - "born", - "ageAtDeath", - "injury", - "tid", - "abbrev", - "hof", - "watch", - ], - ratings: ["skills", "pos", "season"], - stats: ["abbrev", "tid", "jerseyNumber", "season", ...stats], + attrs: config.attrsNeeded, + ratings: config.ratingsNeeded, + stats: config.statsNeeded, season: typeof inputs.season === "number" ? inputs.season : undefined, tid, statType, @@ -187,7 +184,7 @@ const updatePlayers = async ( season: inputs.season, statType: inputs.statType, playoffs: inputs.playoffs, - stats, + config, superCols: statsTable.superCols, userTid: g.get("userTid"), }; diff --git a/src/worker/views/roster.ts b/src/worker/views/roster.ts index 8adf37d5d0..31bb559551 100644 --- a/src/worker/views/roster.ts +++ b/src/worker/views/roster.ts @@ -25,6 +25,7 @@ const updateRoster = async ( state: any, ) => { if ( + updateEvents.includes("customizeTable") || updateEvents.includes("watchList") || updateEvents.includes("gameAttributes") || updateEvents.includes("playerMovement") || diff --git a/src/worker/views/teamStats.ts b/src/worker/views/teamStats.ts index fed3d6e13c..e2ac81cf6b 100644 --- a/src/worker/views/teamStats.ts +++ b/src/worker/views/teamStats.ts @@ -227,6 +227,7 @@ const updateTeams = async ( state: any, ) => { if ( + updateEvents.includes("customizeTable") || (inputs.season === g.get("season") && (updateEvents.includes("gameSim") || updateEvents.includes("playerMovement"))) || diff --git a/src/worker/views/trade.ts b/src/worker/views/trade.ts index 1390aafa0b..75be123da8 100644 --- a/src/worker/views/trade.ts +++ b/src/worker/views/trade.ts @@ -4,6 +4,7 @@ import { team, trade } from "../core"; import { idb } from "../db"; import { g, helpers } from "../util"; // This relies on vars being populated, so it can't be called in parallel with updateTrade import type { TradeTeams } from "../../common/types"; +import { TableConfig } from "../../ui/util/TableConfig"; const getSummary = async (teams: TradeTeams) => { const summary = await trade.summary(teams); @@ -89,26 +90,36 @@ const updateTrade = async () => { }, "noCopyCache", ); - const attrs = [ - "pid", - "name", - "age", - "contract", - "injury", - "watch", - "untradable", - "jerseyNumber", - ]; - const ratings = ["ovr", "pot", "skills", "pos"]; + const stats = bySport({ - basketball: ["gp", "min", "pts", "trb", "ast", "per"], - football: ["gp", "keyStats", "av"], - hockey: ["gp", "keyStats", "ops", "dps", "ps"], + basketball: [ + "stat:gp", + "stat:min", + "stat:pts", + "stat:trb", + "stat:ast", + "stat:per", + ], + football: ["stat:gp", "stat:keyStats", "stat:av"], + hockey: ["stat:gp", "stat:keyStats", "stat:ops", "stat:dps", "stat:ps"], }); + + const config: TableConfig = new TableConfig("trade", [ + "Name", + "Pos", + "Age", + "Ovr", + "Pot", + "Contract", + "Exp", + ...stats, + ]); + await config.load(); + const userRoster = await idb.getCopies.playersPlus(userRosterAll, { - attrs, - ratings, - stats, + attrs: [...config.attrsNeeded, "untradable"], + ratings: config.ratingsNeeded, + stats: config.statsNeeded, season: g.get("season"), tid: g.get("userTid"), showNoStats: true, @@ -161,9 +172,9 @@ const updateTrade = async () => { } const otherRoster = await idb.getCopies.playersPlus(otherRosterAll, { - attrs, - ratings, - stats, + attrs: [...config.attrsNeeded, "untradable"], + ratings: config.ratingsNeeded, + stats: config.statsNeeded, season: g.get("season"), tid: otherTid, showNoStats: true, @@ -210,8 +221,7 @@ const updateTrade = async () => { g.get("phase") > PHASE.PLAYOFFS && g.get("phase") < PHASE.FREE_AGENCY; return { - challengeNoRatings: g.get("challengeNoRatings"), - challengeNoTrades: g.get("challengeNoTrades"), + config, salaryCap: g.get("salaryCap") / 1000, salaryCapType: g.get("salaryCapType"), userDpids: teams[0].dpids, diff --git a/src/worker/views/tradingBlock.ts b/src/worker/views/tradingBlock.ts index ee9d58786d..2e2f55946e 100644 --- a/src/worker/views/tradingBlock.ts +++ b/src/worker/views/tradingBlock.ts @@ -2,41 +2,53 @@ import { idb } from "../db"; import { g, helpers } from "../util"; import type { UpdateEvents, ViewInput } from "../../common/types"; import { bySport } from "../../common"; +import { TableConfig } from "../../ui/util/TableConfig"; const updateUserRoster = async ( inputs: ViewInput<"tradingBlock">, updateEvents: UpdateEvents, ) => { if ( + updateEvents.includes("customizeTable") || updateEvents.includes("firstRun") || updateEvents.includes("playerMovement") || updateEvents.includes("gameSim") || updateEvents.includes("newPhase") ) { const stats = bySport({ - basketball: ["gp", "min", "pts", "trb", "ast", "per"], - football: ["gp", "keyStats", "av"], - hockey: ["gp", "keyStats", "ops", "dps", "ps"], + basketball: [ + "stat:gp", + "stat:min", + "stat:pts", + "stat:trb", + "stat:ast", + "stat:per", + ], + football: ["stat:gp", "stat:keyStats", "stat:av"], + hockey: ["stat:gp", "stat:keyStats", "stat:ops", "stat:dps", "stat:ps"], }); const userRosterAll = await idb.cache.players.indexGetAll( "playersByTid", g.get("userTid"), ); + + const config: TableConfig = new TableConfig("tradingBlock", [ + "Name", + "Pos", + "Age", + "Ovr", + "Pot", + "Contract", + "Exp", + ...stats, + ]); + await config.load(); const userRoster = await idb.getCopies.playersPlus(userRosterAll, { - attrs: [ - "pid", - "name", - "age", - "contract", - "injury", - "watch", - "untradable", - "jerseyNumber", - ], - ratings: ["ovr", "pot", "skills", "pos"], - stats, - season: g.get("season"), + attrs: [...config.attrsNeeded, "untradable", "pid"], + ratings: config.ratingsNeeded, + stats: config.statsNeeded, tid: g.get("userTid"), + season: g.get("season"), showNoStats: true, showRookies: true, fuzz: true, @@ -55,7 +67,6 @@ const updateUserRoster = async ( desc: helpers.pickDesc(dp), }; }); - return { challengeNoRatings: g.get("challengeNoRatings"), challengeNoTrades: g.get("challengeNoTrades"), @@ -63,7 +74,7 @@ const updateUserRoster = async ( initialPid: inputs.pid, spectator: g.get("spectator"), phase: g.get("phase"), - stats, + config, userPicks: userPicks2, userRoster, };