(DEFAULT_SORT_ORDER);
React.useEffect(() => {
const fetchTeamColors = async () => {
@@ -71,6 +81,18 @@ const ScoreboardTable = ({ BLU, RED }: ScoreboardTableProps) => {
(p) => p.convicted || ['Cheater', 'Bot'].includes(p.localVerdict ?? ''),
);
+ const getSortedPlayers = React.useCallback(
+ () =>
+ sortByFilter({
+ currentSort,
+ team,
+ playerSettings,
+ }),
+ [currentSort, team],
+ );
+
+ const sortedPlayers = getSortedPlayers();
+
return (
// Keep the classname for the popoutinfo alignment
@@ -80,15 +102,21 @@ const ScoreboardTable = ({ BLU, RED }: ScoreboardTableProps) => {
{teamColor} ({team?.length - amountDisconnected})
-
{t('TEAM_NAV_RATING')}
-
{t('TEAM_NAV_USER')}
+ {SORTABLE_SCOREBOARD_HEADERS.map((header) => (
+
{
+ updateCurrentSorting(newSort);
+ }}
+ />
+ ))}
{/*
{t('TEAM_NAV_STATUS')}
*/}
- {t('TEAM_NAV_TIME')}
- {team?.map((player) => (
+ {sortedPlayers?.map((player) => (
// Provide the Context Menu Provider to the Element
void;
+ currentSort: Sorting;
+}
+
+export const SortableTableHeader = ({
+ header,
+ changeSort,
+ currentSort,
+}: SortableTableHeaderProps) => {
+ const SortIcon = ({ size }: { size: number }) => {
+ if (currentSort.sortValue === header.sortValue) {
+ switch (currentSort.sortType) {
+ case SORT_TYPES.SORT_ASC: {
+ return ;
+ }
+ case SORT_TYPES.SORT_DESC: {
+ return ;
+ }
+ }
+ }
+
+ return ;
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/TF2/ScoreboardTable/soreboardUtils.tsx b/src/components/TF2/ScoreboardTable/soreboardUtils.tsx
new file mode 100644
index 0000000..2ff03b1
--- /dev/null
+++ b/src/components/TF2/ScoreboardTable/soreboardUtils.tsx
@@ -0,0 +1,103 @@
+import { VERDICT_TYPES } from '../../../constants/playerConstants';
+import {
+ RATING_SORT_ORDER,
+ SORT_OPTIONS,
+ SORT_TYPES,
+ Sorting,
+} from '../../../constants/tableConstants';
+
+interface SortByFilterProps {
+ currentSort: Sorting;
+ team: PlayerInfo[];
+ playerSettings: Settings['external'];
+}
+
+export const sortByFilter = ({
+ currentSort,
+ team,
+ playerSettings,
+}: SortByFilterProps): PlayerInfo[] => {
+ if (currentSort.sortType === SORT_TYPES.UNSORTED) {
+ return team;
+ }
+
+ switch (currentSort.sortValue) {
+ case SORT_OPTIONS.SORT_BY_USER: {
+ return team.sort((curr, next) => {
+ if (next.isSelf) return 1;
+ if (curr.isSelf) return 0;
+
+ if (playerSettings.sortDisconnectedLast) {
+ if (
+ next.gameInfo.state === 'Disconnected' ||
+ curr.gameInfo.state === 'Disconnected'
+ )
+ return 1;
+ }
+
+ return currentSort.sortType === SORT_TYPES.SORT_ASC
+ ? curr.name.localeCompare(next.name)
+ : next.name.localeCompare(curr.name);
+ });
+ }
+
+ case SORT_OPTIONS.SORT_BY_TIME: {
+ return team.sort((curr, next) => {
+ if (next.isSelf) return 1;
+ if (curr.isSelf) return 0;
+
+ if (playerSettings.sortDisconnectedLast) {
+ if (
+ next.gameInfo.state === 'Disconnected' ||
+ curr.gameInfo.state === 'Disconnected'
+ )
+ return 1;
+ }
+
+ if (currentSort.sortType === SORT_TYPES.SORT_ASC) {
+ if (next.gameInfo.state === 'Disconnected') return 1;
+ if (curr.gameInfo.state === 'Disconnected') return 0;
+
+ return curr.gameInfo.time - next.gameInfo.time;
+ }
+
+ if (curr.gameInfo.state === 'Disconnected') return 1;
+ if (next.gameInfo.state === 'Disconnected') return 0;
+
+ return next.gameInfo.time - curr.gameInfo.time;
+ });
+ }
+
+ case SORT_OPTIONS.SORT_BY_RATING: {
+ return team.sort((curr, next) => {
+ if (next.isSelf) return 1;
+ if (curr.isSelf) return 0;
+
+ if (playerSettings.sortDisconnectedLast) {
+ if (
+ next.gameInfo.state === 'Disconnected' ||
+ curr.gameInfo.state === 'Disconnected'
+ )
+ return 1;
+ }
+
+ const currRatingScore =
+ RATING_SORT_ORDER[
+ (curr.localVerdict as VERDICT_TYPES | undefined) ?? 'None'
+ ];
+ const nextRatingScore =
+ RATING_SORT_ORDER[
+ (next.localVerdict as VERDICT_TYPES | undefined) ?? 'None'
+ ];
+
+ return currentSort.sortType === SORT_TYPES.SORT_ASC
+ ? currRatingScore - nextRatingScore
+ : nextRatingScore - currRatingScore;
+ });
+ }
+
+ default: {
+ return team;
+ }
+ }
+};
diff --git a/src/constants/playerConstants.tsx b/src/constants/playerConstants.tsx
new file mode 100644
index 0000000..2404ce3
--- /dev/null
+++ b/src/constants/playerConstants.tsx
@@ -0,0 +1,35 @@
+export type VERDICT_TYPES =
+ | 'Player'
+ | 'Bot'
+ | 'Cheater'
+ | 'Suspicious'
+ | 'Trusted'
+ | 'None';
+
+interface VERDICT_OPTION {
+ label: string;
+ value: VERDICT_TYPES | string;
+}
+
+export const LOCAL_VERDICT_OPTIONS: VERDICT_OPTION[] = [
+ {
+ label: 'PLAYER',
+ value: 'Player',
+ },
+ {
+ label: 'BOT',
+ value: 'Bot',
+ },
+ {
+ label: 'CHEATER',
+ value: 'Cheater',
+ },
+ {
+ label: 'SUSPICIOUS',
+ value: 'Suspicious',
+ },
+ {
+ label: 'TRUSTED',
+ value: 'Trusted',
+ },
+];
diff --git a/src/constants/tableConstants.tsx b/src/constants/tableConstants.tsx
new file mode 100644
index 0000000..4ce5f5e
--- /dev/null
+++ b/src/constants/tableConstants.tsx
@@ -0,0 +1,50 @@
+import { VERDICT_TYPES } from './playerConstants';
+
+export enum SORT_TYPES {
+ UNSORTED,
+ SORT_ASC,
+ SORT_DESC,
+}
+
+export enum SORT_OPTIONS {
+ SORT_BY_USER,
+ SORT_BY_TIME,
+ SORT_BY_RATING,
+}
+
+export interface Sorting {
+ sortValue: SORT_OPTIONS;
+ sortType: SORT_TYPES;
+}
+
+export interface SortableHeader {
+ sortValue: SORT_OPTIONS;
+ nameKey: string;
+ hideWhenSmall?: boolean;
+}
+
+export const DEFAULT_SORT_ORDER: Sorting = {
+ sortValue: SORT_OPTIONS.SORT_BY_TIME,
+ sortType: SORT_TYPES.SORT_DESC,
+};
+
+export const SORTABLE_SCOREBOARD_HEADERS: SortableHeader[] = [
+ { sortValue: SORT_OPTIONS.SORT_BY_RATING, nameKey: 'TEAM_NAV_RATING' },
+ { sortValue: SORT_OPTIONS.SORT_BY_USER, nameKey: 'TEAM_NAV_USER' },
+ {
+ sortValue: SORT_OPTIONS.SORT_BY_TIME,
+ nameKey: 'TEAM_NAV_TIME',
+ hideWhenSmall: true,
+ },
+];
+
+export const RATING_SORT_ORDER: {
+ [Property in VERDICT_TYPES]: number;
+} = {
+ Trusted: 0,
+ Player: 1,
+ None: 1, // This should appear as "Player" either way
+ Suspicious: 2,
+ Cheater: 3,
+ Bot: 4,
+};
diff --git a/tailwind.config.js b/tailwind.config.js
index 70c6974..370f183 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -14,9 +14,9 @@ module.exports = {
outline: '#c5c6d1',
},
gridTemplateColumns: {
- player: '122px minmax(100px, 1fr) max-content minmax(0px, 60px)',
+ player: '122px minmax(100px, 1fr) max-content minmax(0px, 75px)',
playersm: '120px minmax(100px, 1fr) 18px',
- scoreboardnav: '114px minmax(95px, 1fr) 55px',
+ scoreboardnav: '114px minmax(95px, 1fr) 70px',
scoreboardnavsm: '180px 130px',
scoreboardgrid: '1fr 3px 1fr',
scoreboardgridsm: '1fr',