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

feat: Add basic statistics page with weapon information for factions #82

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
87 changes: 65 additions & 22 deletions components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ActionIcon,
Tooltip,
Anchor,
Flex,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import Image from "next/image";
Expand All @@ -26,6 +27,8 @@ import { SearchButton } from "../search-button/search-button";
import { OnlinePlayers } from "../online-players";
import { raceType } from "../../src/coh3/coh3-types";
import { localizedNames } from "../../src/coh3/coh3-data";
import FactionIcon from "../faction-icon";
import config from "../../config";

export interface HeaderProps {
// children?: React.ReactNode;
Expand Down Expand Up @@ -114,26 +117,30 @@ const useStyles = createStyles((theme) => ({
},
}));

const FactionLink = ({ faction, href }: { faction: raceType; href: string }) => (
<Text>
<Anchor component={Link} href={href}>
{localizedNames[faction]}
</Anchor>
</Text>
);

export const Header: React.FC<HeaderProps> = () => {
const { classes, cx } = useStyles();
const [opened, { toggle, close }] = useDisclosure(false);

const factionLink = (faction: raceType, gamemode: string) => (
<Text>
<Anchor component={Link} href={"/leaderboards?race=" + faction + "&type=" + gamemode}>
{localizedNames[faction]}
</Anchor>
</Text>
const factionLeaderboardLink = (faction: raceType, gamemode: string) => (
<FactionLink faction={faction} href={"/leaderboards?race=" + faction + "&type=" + gamemode} />
);

const gamemodeLeaderboards = (gamemode: string) => (
<div>
<Text weight={700}>{gamemode}</Text>
<Divider my="sm" />
{factionLink("american", gamemode)}
{factionLink("german", gamemode)}
{factionLink("dak", gamemode)}
{factionLink("british", gamemode)}
{factionLeaderboardLink("american", gamemode)}
{factionLeaderboardLink("german", gamemode)}
{factionLeaderboardLink("dak", gamemode)}
{factionLeaderboardLink("british", gamemode)}
</div>
);
return (
Expand Down Expand Up @@ -176,18 +183,54 @@ export const Header: React.FC<HeaderProps> = () => {
</SimpleGrid>
</HoverCard.Dropdown>
</HoverCard>
<Tooltip label="Coming soon" color="orange">
<Anchor
component={Link}
href="#"
className={cx(classes.link, classes.disabledLink)}
>
Statistics{" "}
<ActionIcon color="orange" size="sm" radius="xl" variant="transparent">
<IconBarrierBlock size={16} />
</ActionIcon>
</Anchor>
</Tooltip>

{config.isDevEnv() ? (
<HoverCard width={800} position="bottom" radius="md" shadow="md" withinPortal>
<HoverCard.Target>
<div>
<Anchor component={Link} href="/statistics" className={cx(classes.link)}>
<Group spacing={3}>
Statistics
<IconChevronDown size={16} />
</Group>
</Anchor>
</div>
</HoverCard.Target>
<HoverCard.Dropdown sx={{ overflow: "hidden" }}>
<Flex justify="space-between">
<Flex gap="xs" align="center">
<FactionIcon name="american" width={18} />
<FactionLink faction="american" href="/statistics/american" />
</Flex>
<Flex gap="xs" align="center">
<FactionIcon name="british" width={18} />
<FactionLink faction="british" href="/statistics/british" />
</Flex>
<Flex gap="xs" align="center">
<FactionIcon name="dak" width={18} />
<FactionLink faction="dak" href="/statistics/dak" />
</Flex>
<Flex gap="xs" align="center">
<FactionIcon name="german" width={18} />
<FactionLink faction="german" href="/statistics/german" />
</Flex>
</Flex>
</HoverCard.Dropdown>
</HoverCard>
) : (
<Tooltip label="Coming soon" color="orange">
<Anchor
component={Link}
href="#"
className={cx(classes.link, classes.disabledLink)}
>
Statistics{" "}
<ActionIcon color="orange" size="sm" radius="xl" variant="transparent">
<IconBarrierBlock size={16} />
</ActionIcon>
</Anchor>
</Tooltip>
)}

<Tooltip label="Coming soon" color="orange">
<Anchor
Expand Down
10 changes: 9 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ const { withEdgio, withServiceWorker } = require("@edgio/next/config");
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
// Maybe fixes rehydration issues with Mantine components - should verify this before merging into master
styledComponents: true,
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
largePageDataBytes: 10 * 1024 * 1024,
experimental: {
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
largePageDataBytes: 10 * 1024 * 1024,
},
};

const _preEdgioExport = nextConfig;

module.exports = (phase, config) =>
module.exports = async () =>
withEdgio(
withServiceWorker({
// Output sourcemaps so that stack traces have original source filenames and line numbers when tailing
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@
"jest-environment-jsdom": "^29.4.3",
"lint-staged": "^13.1.1",
"prettier": "^2.8.4",
"typescript": "4.8.4"
"typescript": "4.9.5"
}
}
39 changes: 39 additions & 0 deletions pages/statistics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextPage } from "next";
import Head from "next/head";

import { Container, Title, Group, Anchor, Flex } from "@mantine/core";
import { localizedNames } from "../src/coh3/coh3-data";
import FactionIcon from "../components/faction-icon";
import Link from "next/link";

const Explorer: NextPage = () => {
const factions = Object.keys(localizedNames) as Array<keyof typeof localizedNames>;

return (
<div>
<Head>
<title>Explorer</title>
<meta name="description" content="COH3 Stats - learn more about our page." />
</Head>
<>
<Container size={"md"}>
<Title order={1} size="h4" pt="md">
Statistics
</Title>
<Group>
{factions.map((link) => (
<Anchor key={link} component={Link} href={`/statistics/${link}`}>
<Flex>
<FactionIcon name={link} width={40} />
<Group spacing="xs">{localizedNames[link]}</Group>
</Flex>
</Anchor>
))}
</Group>
</Container>
</>
</div>
);
};

export default Explorer;
160 changes: 160 additions & 0 deletions pages/statistics/[faction].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Container, Flex, Table, Title, TextInput } from "@mantine/core";
import Head from "next/head";
import { GetStaticPaths, GetStaticProps } from "next";
import { localizedNames } from "../../src/coh3/coh3-data";
import { normalizeWeapons } from "../../src/lib/weapon-normalizer";
import FactionIcon from "../../components/faction-icon";
import { useState } from "react";
import { IconSearch } from "@tabler/icons";
import { keys } from "lodash";
import { raceType } from "../../src/coh3/coh3-types";
import { NullableNormalizedWeapon } from "../../src/lib/types";

function filterData(data: NullableNormalizedWeapon[], search: string) {
const query = search.toLowerCase().trim();

return data.filter((item) =>
keys(data[0]).some((key) => {
const value = item[key as keyof NullableNormalizedWeapon];
return value && typeof value === "string" && value.toLowerCase().includes(query);
}),
);
}

export interface FactionProps {
faction?: raceType;
weapons?: NullableNormalizedWeapon[];
}

const Faction = ({ faction, weapons }: FactionProps) => {
console.log(faction);
const [search, setSearch] = useState("");
const wehrmachtWeapons = weapons?.filter((weapon) =>
// compensating for a little mismatch between our raceType and the game's name for DAK
faction === "dak" ? weapon.owner === "afrika_korps" : weapon.owner === faction,
);

const [sortedData, setSortedData] = useState(wehrmachtWeapons);

if (!weapons || !faction) return null;

const name = (w: NullableNormalizedWeapon) =>
w.displayName ? `${w.displayName} - ${w.referenceName}` : w.referenceName;

const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (wehrmachtWeapons === undefined) return;

const { value } = event.currentTarget;
setSearch(value);
setSortedData(filterData(wehrmachtWeapons, value));
};

const ths = (
<tr>
<th>Name</th>
<th>Category</th>
<th>Type</th>
<th>Subtype</th>
<th>Accuracy Far</th>
<th>Accuracy Mid</th>
<th>Accuracy Near</th>
</tr>
);

const rows = sortedData?.map((element) => (
<tr key={name(element)}>
<td>{name(element)}</td>
<td>{element.category}</td>
<td>{element.type}</td>
<td>{element.subtype}</td>
<td>{element.accuracy?.far}</td>
<td>{element.accuracy?.mid}</td>
<td>{element.accuracy?.near}</td>
</tr>
));

return (
<div>
<Head>
<title>Explorer</title>
<meta name="description" content="COH3 Stats - learn more about our page." />
</Head>
<>
<Container size={"xl"}>
<Flex align="center" mb={50}>
<FactionIcon name={faction} width={40} />
<Title order={1} size="h4" pt="md" ml={10}>
{faction && localizedNames[faction]} Weapons
</Title>
</Flex>
<TextInput
placeholder="Search by any field"
mb="md"
icon={<IconSearch size="0.9rem" stroke={1.5} />}
value={search}
onChange={handleSearchChange}
/>
<Table
captionSide="bottom"
striped
withBorder
highlightOnHover
horizontalSpacing="lg"
miw={900}
>
<caption>{localizedNames[faction]} Weapons</caption>
<thead>{ths}</thead>
<tbody>{rows}</tbody>
</Table>
</Container>
</>
</div>
);
};

export const getStaticPaths: GetStaticPaths<{ faction: raceType }> = async () => {
return {
paths: [
{
params: { faction: "american" },
},
{
params: { faction: "german" },
},
{
params: { faction: "british" },
},
{
params: { faction: "dak" },
},
],
fallback: false,
};
};

export const getStaticProps: GetStaticProps<FactionProps> = async ({ params }) => {
const faction = params?.faction as raceType | undefined;

const weaponsReq = await fetch(
"https://github.com/cohstats/coh3-data/raw/xml-data/scripts/xml-to-json/exported/weapon.json",
);

const locstringReq = await fetch(
"https://github.com/cohstats/coh3-data/raw/xml-data/scripts/xml-to-json/exported/locstring.json",
);

const weaponData = await weaponsReq.json();
const locstringData = await locstringReq.json();

const flattenedWeapons = normalizeWeapons(weaponData, locstringData);

return {
props: {
weapons: flattenedWeapons,
faction,
key: faction,
},
};
};

export default Faction;
Loading