Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions i18n/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"PREF_FRIEND_API_USAGE_ALL": "All Players",
"PREF_FRIEND_API_USAGE_CHEATERS_ONLY": "Cheaters",
"PREF_FRIEND_API_USAGE_NONE": "Nobody",
"PREF_SORT_DISCONNECTED_LAST": "Force disconnected players to appear last",
"TOOLTIP_ACTUAL_NAME": "Actual Name:",
"TOOLTIP_PREVIOUS_NAME": "Previous Name(s):",
"TOOLTIP_BANS_DAYS": "day(s) since last ban",
Expand Down
9 changes: 9 additions & 0 deletions src/Pages/Preferences/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ const Preferences = () => {
onChange={(e) => handleSettingChange('openInApp', e)}
/>
</Flex>
<Flex className="preference-option">
<div className="preference-title">
{t('PREF_SORT_DISCONNECTED_LAST')}
</div>
<Checkbox
checked={settings?.external.sortDisconnectedLast}
onChange={(e) => handleSettingChange('sortDisconnectedLast', e)}
/>
</Flex>
</Accordion>
<Accordion title={t('PREF_COLORS')} className="preference-accordion">
<TextItem className="mr-9">{t('PREF_COLORS_PRECEDENCE')}</TextItem>
Expand Down
1 change: 1 addition & 0 deletions src/api/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface Settings {
external: {
language?: string;
openInApp?: boolean;
sortDisconnectedLast?: boolean;
colors?: {
You: string;
Player: string;
Expand Down
2 changes: 2 additions & 0 deletions src/api/preferences/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ interface PreferenceResponse {
interface Settings {
language: string;
openInApp: boolean;
sortDisconnectedLast: boolean;
colors: {
You: string;
Player: string;
Expand All @@ -82,6 +83,7 @@ export const defaultSettings: PreferenceResponse = {
external: {
language: 'English',
openInApp: false,
sortDisconnectedLast: true,
colors: {
You: '#00aaaa',
Player: 'none',
Expand Down
6 changes: 1 addition & 5 deletions src/components/General/Checkbox/Checkbox.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
.checkbox-wrapper {
display: flex;
align-items: center;
cursor: pointer;
}

.checkbox-icon {
position: relative;
top: 6px;
}

.checkbox-icon.checked {
color: #0dc9d0;
}
Expand Down
30 changes: 5 additions & 25 deletions src/components/TF2/Player/playerutils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,7 @@ import {
Users2,
} from 'lucide-react';
import { Tooltip } from '@components/General';

const localVerdict = [
{
label: 'PLAYER',
value: 'Player',
},
{
label: 'BOT',
value: 'Bot',
},
{
label: 'CHEATER',
value: 'Cheater',
},
{
label: 'SUSPICIOUS',
value: 'Suspicious',
},
{
label: 'TRUSTED',
value: 'Trusted',
},
];
import { LOCAL_VERDICT_OPTIONS } from '../../../constants/playerConstants';

function calculateKD(kills: number = 0, deaths: number = 0): string {
// No Kills, No KD
Expand All @@ -47,15 +25,17 @@ function calculateKD(kills: number = 0, deaths: number = 0): string {
function localizeVerdict(verdict: string | undefined) {
if (!verdict || verdict.toLowerCase() === 'none') return t('PLAYER');

const option = localVerdict.find((option) => option.value === verdict);
const option = LOCAL_VERDICT_OPTIONS.find(
(option) => option.value === verdict,
);

if (!option) console.error('Invalid verdict: ', verdict);

return option ? t(option.label.toUpperCase()) : t('PLAYER');
}

function makeLocalizedVerdictOptions() {
return localVerdict.map((option) => ({
return LOCAL_VERDICT_OPTIONS.map((option) => ({
label: t(option.label.toUpperCase()),
value: option.value,
}));
Expand Down
40 changes: 34 additions & 6 deletions src/components/TF2/ScoreboardTable/ScoreboardTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import React from 'react';
import './ScoreboardTable.css';

import { getAllSettings } from '@api/preferences';
import { Player } from '@components/TF2';
import { t } from '@i18n';
import { ContextMenuProvider } from '@context';
import {
DEFAULT_SORT_ORDER,
SORTABLE_SCOREBOARD_HEADERS,
Sorting,
} from '../../../constants/tableConstants';
import { SortableTableHeader } from './SortableTableHeader';
import { sortByFilter } from './soreboardUtils';

import './ScoreboardTable.css';

interface ScoreboardTableProps {
RED: PlayerInfo[];
BLU: PlayerInfo[];
Expand All @@ -29,6 +37,8 @@ const ScoreboardTable = ({ BLU, RED }: ScoreboardTableProps) => {
},
openInApp: false,
});
const [currentSort, updateCurrentSorting] =
React.useState<Sorting>(DEFAULT_SORT_ORDER);

React.useEffect(() => {
const fetchTeamColors = async () => {
Expand Down Expand Up @@ -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
<div className={`scoreboard-grid-half ${teamColor}`}>
Expand All @@ -80,15 +102,21 @@ const ScoreboardTable = ({ BLU, RED }: ScoreboardTableProps) => {
{teamColor} ({team?.length - amountDisconnected})
</div>
<div className="flex-1 ml-5 mb-5 text-start font-build grid grid-cols-scoreboardnavsm xs:grid-cols-scoreboardnav">
<div>{t('TEAM_NAV_RATING')}</div>
<div>{t('TEAM_NAV_USER')}</div>
{SORTABLE_SCOREBOARD_HEADERS.map((header) => (
<SortableTableHeader
header={header}
currentSort={currentSort}
changeSort={(newSort: Sorting) => {
updateCurrentSorting(newSort);
}}
/>
))}
{/* <div className="hidden xs:[display:unset]">
{t('TEAM_NAV_STATUS')}
</div> */}
<div className="hidden xs:[display:unset]">{t('TEAM_NAV_TIME')}</div>
</div>
<div className={`${teamColor?.toLowerCase()}`}>
{team?.map((player) => (
{sortedPlayers?.map((player) => (
// Provide the Context Menu Provider to the Element
<ContextMenuProvider key={player.steamID64}>
<Player
Expand Down
62 changes: 62 additions & 0 deletions src/components/TF2/ScoreboardTable/SortableTableHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';

import { t } from '@i18n';
import {
ArrowDownUp,
ArrowDownWideNarrow,
ArrowUpNarrowWide,
} from 'lucide-react';
import {
SORT_TYPES,
SortableHeader,
Sorting,
} from '../../../constants/tableConstants';

interface SortableTableHeaderProps {
header: SortableHeader;
changeSort: (sorting: Sorting) => 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 <ArrowUpNarrowWide size={size} />;
}
case SORT_TYPES.SORT_DESC: {
return <ArrowDownWideNarrow size={size} />;
}
}
}

return <ArrowDownUp size={size} />;
};

return (
<div>
<button
className={`gap-1 items-center ${
header?.hideWhenSmall ? 'xs:flex hidden' : 'flex'
}`}
onClick={() => {
changeSort({
sortValue: header.sortValue,
sortType:
currentSort.sortType === SORT_TYPES.SORT_ASC
? SORT_TYPES.SORT_DESC
: SORT_TYPES.SORT_ASC,
});
}}
>
{t(header.nameKey)}
<SortIcon size={16} />
</button>
</div>
);
};
103 changes: 103 additions & 0 deletions src/components/TF2/ScoreboardTable/soreboardUtils.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
}
};
35 changes: 35 additions & 0 deletions src/constants/playerConstants.tsx
Original file line number Diff line number Diff line change
@@ -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',
},
];
Loading