Skip to content

Commit

Permalink
Filter recent matches (#93)
Browse files Browse the repository at this point in the history
* Add customizable headers, interfaces for match history

* actually apply filters

* Prettier

* Graphical improvements

---------

Co-authored-by: Ryan Sullivan <mrsulliv@gmail.com>
  • Loading branch information
petrvecera and rysllvn authored Mar 9, 2023
1 parent b8b7bb7 commit daa1a25
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 33 deletions.
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[];
}

0 comments on commit daa1a25

Please sign in to comment.