Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter recent matches #93

Merged
merged 4 commits into from
Mar 9, 2023
Merged
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
52 changes: 52 additions & 0 deletions components/player-card/filterable-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Button, Checkbox, Flex, Group, List, Popover, Space, Text } from "@mantine/core";
import { IconFilter } from "@tabler/icons";
import { FilterInformation } from "./player-recent-matches";

type FilterableHeaderProps = {
title: string;
options: { [key: string | number]: FilterInformation };
onChange: (filter: string | number) => void;
onReset: () => void;
};
const FilterableHeader = ({ options, title, onChange, onReset }: FilterableHeaderProps) => {
function handleCheckboxChange(filter: string | number) {
onChange(filter);
}

return (
<Popover position="bottom" withArrow shadow="md">
<Popover.Target>
<Group position="center">
<Flex>
<Text>{title}</Text>
<IconFilter cursor="pointer" />
</Flex>
</Group>
</Popover.Target>
<Popover.Dropdown>
<List listStyleType="none" style={{ textAlign: "left" }}>
{Object.entries(options).map(([filter, { checked, label }]) => {
return (
<List.Item key={label}>
<Checkbox
label={label}
checked={checked}
styles={{ input: { cursor: "pointer" }, label: { cursor: "pointer" } }}
onChange={() => handleCheckboxChange(filter)}
/>
</List.Item>
);
})}
</List>
<Space h={"xs"} />
<Group position="left">
<Button onClick={onReset} size="xs">
Reset
</Button>
</Group>
</Popover.Dropdown>
</Popover>
);
};

export default FilterableHeader;
169 changes: 136 additions & 33 deletions components/player-card/player-recent-matches.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,135 @@ import dynamic from "next/dynamic";
import { DataTable, DataTableSortStatus } from "mantine-datatable";
import React from "react";
import { maps, matchTypesAsObject, raceIDs } from "../../src/coh3/coh3-data";
import { raceID } from "../../src/coh3/coh3-types";
import { MatchHistory, raceID } from "../../src/coh3/coh3-types";
import { getMatchDuration, getMatchPlayersByFaction } from "../../src/coh3/helpers";
import ErrorCard from "../error-card";
import FactionIcon from "../faction-icon";
import { IconInfoCircle } from "@tabler/icons";
import sortBy from "lodash/sortBy";
import cloneDeep from "lodash/cloneDeep";
import config from "../../config";
import FilterableHeader from "./filterable-header";

/**
* Timeago is causing issues with SSR, move to client side
*/
const DynamicTimeAgo = dynamic(() => import("../../components/internal-timeago"), {
ssr: false,
// @ts-ignore
loading: () => "Calculating...",
});

export type FilterInformation = { label: string; checked: boolean; filter: string | number };

const PlayerRecentMatches = ({
profileID,
playerMatchesData,
error,
}: {
profileID: string;
playerMatchesData: Array<any>;
playerMatchesData: Array<MatchHistory>;
error: string;
}) => {
const isPlayerVictorious = (matchRecord: any): boolean => {
if (!matchRecord) return false;

const playerResult = getPlayerMatchHistoryResult(matchRecord);
return playerResult.resulttype === 1;
};

const getPlayerMatchHistoryResult = (matchRecord: any) => {
for (const record of matchRecord.matchhistoryreportresults) {
if (`${record.profile_id}` === `${profileID}`) {
return record;
}
}

return matchRecord.matchhistoryreportresults[0];
};
const [sortStatus, setSortStatus] = React.useState<DataTableSortStatus>({
columnAccessor: "Played",
direction: "asc",
});
const [filters, setFilters] = React.useState<{
[key: string]: { [key: string | number]: FilterInformation };
}>({
result: {},
map: {},
mode: {},
});

// filters then sorts the table
const sortedData = React.useMemo(() => {
// go through all filters and add only unchecked filters
const toExclude: { [key: string]: string[] } = {};
Object.entries(filters).forEach(([column, filterMap]) => {
toExclude[column] = [];
Object.entries(filterMap).forEach(([filter, filterInfo]) => {
if (!filterInfo.checked) toExclude[column].push(filter);
});
});

const resortedData = sortBy(
playerMatchesData,
sortStatus.columnAccessor === "match_duration"
? (matchData) => {
return matchData.startgametime - matchData.completiontime;
}
: sortStatus.columnAccessor,
);
).filter((matchData) => {
// checking the status of the record to filter is different for each column type so
// thats why this messy logic is here
let include = true;
toExclude["result"]?.forEach((filter) => {
if (filter === "victory" && isPlayerVictorious(matchData)) include = false;
if (filter === "defeat" && !isPlayerVictorious(matchData)) include = false;
});
toExclude["map"].forEach((map: string) => {
if (matchData.mapname === map) include = false;
});
toExclude["mode"].forEach((matchtype_id) => {
if (matchData.matchtype_id.toString() === matchtype_id) include = false;
});

return include;
});
return sortStatus.direction === "desc" ? resortedData.reverse() : resortedData;
}, [sortStatus, playerMatchesData]);
// eslint wants isplayervictorious in here. I think this will make this run on every render
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortStatus, playerMatchesData, filters]);

// populate filters with values actually found in this players recent history
React.useEffect(() => {
const mapNameMap: { [key: string]: FilterInformation } = {};
const matchTypeMap: { [key: number]: FilterInformation } = {};
playerMatchesData?.forEach(({ mapname, matchtype_id }) => {
mapNameMap[mapname] = {
label: maps[mapname]?.name || mapname,
checked: true,
filter: mapname,
};
matchTypeMap[matchtype_id] = {
label: (
matchTypesAsObject[matchtype_id]["localizedName"] ||
matchTypesAsObject[matchtype_id]["name"] ||
"unknown"
).toLowerCase(),
checked: true,
filter: matchtype_id,
};
});

const updatedFilters = {
result: {
victory: { label: "victory", checked: true, filter: "victory" },
defeat: { label: "defeat", checked: true, filter: "defeat" },
},
map: mapNameMap,
mode: matchTypeMap,
};
setFilters(updatedFilters);
}, [playerMatchesData]);

if (error) {
return <ErrorCard title={"Error rendering recent matches"} body={JSON.stringify(error)} />;
Expand All @@ -52,32 +148,6 @@ const PlayerRecentMatches = ({
);
}

/**
* Timeago is causing issues with SSR, move to client side
*/
const DynamicTimeAgo = dynamic(() => import("../../components/internal-timeago"), {
ssr: false,
// @ts-ignore
loading: () => "Calculating...",
});

const isPlayerVictorious = (matchRecord: any): boolean => {
if (!matchRecord) return false;

const playerResult = getPlayerMatchHistoryResult(matchRecord);
return playerResult.resulttype === 1;
};

const getPlayerMatchHistoryResult = (matchRecord: any) => {
for (const record of matchRecord.matchhistoryreportresults) {
if (`${record.profile_id}` === `${profileID}`) {
return record;
}
}

return matchRecord.matchhistoryreportresults[0];
};

const renderMap = (name: string) => {
// In case we don't track the map, eg custom maps
if (!maps[name]) {
Expand Down Expand Up @@ -143,6 +213,18 @@ const PlayerRecentMatches = ({
);
};

const handleFilterChange = (column: string, filter: string | number) => {
const updatedFilters = cloneDeep(filters);
updatedFilters[column][filter].checked = !updatedFilters[column][filter].checked;
setFilters(updatedFilters);
};

const handleFilterReset = (column: string) => {
const updatedFilters = cloneDeep(filters);
Object.values(updatedFilters[column]).forEach((filter) => (filter.checked = true));
setFilters(updatedFilters);
};

return (
<>
<DataTable
Expand Down Expand Up @@ -177,7 +259,14 @@ const PlayerRecentMatches = ({
},
{
accessor: "result",
title: "Result",
title: (
<FilterableHeader
title="Result"
options={filters.result}
onChange={(filter) => handleFilterChange("result", filter)}
onReset={() => handleFilterReset("result")}
/>
),
textAlignment: "center",
render: (record) => {
if (isPlayerVictorious(record)) {
Expand Down Expand Up @@ -225,15 +314,29 @@ const PlayerRecentMatches = ({
},
{
accessor: "mapname",
title: "Map",
title: (
<FilterableHeader
title="Map"
options={filters.map}
onChange={(filter) => handleFilterChange("map", filter)}
onReset={() => handleFilterReset("map")}
/>
),
// sortable: true,
textAlignment: "center",
render: (record) => {
return <>{renderMap(record.mapname)}</>;
},
},
{
title: "Mode",
title: (
<FilterableHeader
title="Mode"
options={filters.mode}
onChange={(filter) => handleFilterChange("mode", filter)}
onReset={() => handleFilterReset("mode")}
/>
),
accessor: "matchtype_id",
// sortable: true,
textAlignment: "center",
Expand Down
49 changes: 49 additions & 0 deletions src/coh3/coh3-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,52 @@ export type PlayerCardDataType = {
standings: InternalStandings;
info: { country: string; level: number; name: string; xp: number | undefined };
};

export interface Matchhistoryreportresult {
matchhistory_id: number;
profile_id: number;
resulttype: number;
teamid: number;
race_id: number;
counters: string;
profile: RawPlayerProfile;
}

export interface Matchhistoryitem {
profile_id: number;
iteminstance_id: number;
itemdefinition_id: number;
itemlocation_id: number;
}

export interface Matchhistorymember {
matchhistory_id: number;
profile_id: number;
race_id: number;
statgroup_id: number;
teamid: number;
wins: number;
losses: number;
streak: number;
arbitration: number;
outcome: number;
oldrating: number;
newrating: number;
reporttype: number;
}

export interface MatchHistory {
id: number;
creator_profile_id: number;
mapname: string;
maxplayers: number;
matchtype_id: number;
description: string;
startgametime: number;
completiontime: number;
matchhistoryreportresults: Matchhistoryreportresult[];
matchhistoryitems: Matchhistoryitem[];
matchhistorymember: Matchhistorymember[];
profile_ids: number[];
steam_ids: string[];
}