Skip to content

Commit

Permalink
Add Player Export API page (#254)
Browse files Browse the repository at this point in the history
* Add player export page

* Correct status code
  • Loading branch information
petrvecera authored Sep 11, 2023
1 parent 94c4e8c commit 77989fb
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 22 deletions.
6 changes: 3 additions & 3 deletions __tests__/pages/api/playerExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,21 @@ describe("playerExportAPIHandler", () => {
};
await handler(req as unknown as NextApiRequest, res as unknown as NextApiResponse);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: "profile id contains invalid params" });
expect(res.json).toHaveBeenCalledWith({ error: "profile id contains invalid data" });
});

test("should return 500 if too many records requested", async () => {
const req = {
query: {
profileIDs: JSON.stringify(Array(101).fill(1)),
profileIDs: JSON.stringify(Array(51).fill(1)),
},
};
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
await handler(req as unknown as NextApiRequest, res as unknown as NextApiResponse);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({ error: "Too many records requested" });
});

Expand Down
13 changes: 12 additions & 1 deletion components/Header/components/OtherMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ import {
Tooltip,
} from "@mantine/core";
import Link from "next/link";
import { getOpenDataRoute, getRankingTiersRoute } from "../../../src/routes";
import {
getOpenDataRoute,
getPlayerExportRoute,
getRankingTiersRoute,
} from "../../../src/routes";
import React from "react";
import {
IconActivity,
IconBarrierBlock,
IconChevronDown,
IconDatabaseShare,
IconAward,
IconUsers,
} from "@tabler/icons-react";

const OtherMenu = ({
Expand Down Expand Up @@ -92,6 +97,12 @@ const OtherMenu = ({
Open Data
</Anchor>
</Group>
<Group spacing={"xs"}>
<IconUsers size={16} />
<Anchor component={Link} href={getPlayerExportRoute()}>
Player Export API
</Anchor>
</Group>
<Group spacing={"xs"}>
<IconActivity size={16} />
<Anchor
Expand Down
5 changes: 5 additions & 0 deletions pages/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { IconBarrierBlock } from "@tabler/icons-react";
import React, { useEffect } from "react";
import { AnalyticsAboutAppPageView } from "../src/firebase/analytics";
import config from "../config";
import { generateKeywordsString } from "../src/head-utils";

const keywords = generateKeywordsString(["coh3 stats", "coh3 discord", "bug report", "github"]);

/**
* This is example page you can find it by going on ur /example
Expand All @@ -25,6 +28,8 @@ const About: NextPage = () => {
<Head>
<title>About COH3 Stats</title>
<meta name="description" content="COH3 Stats - learn more about our page." />
<meta name="keywords" content={keywords} />
<meta property="og:image" content={`/logo/android-icon-192x192.png`} />
</Head>
<>
<Container size={"md"}>
Expand Down
41 changes: 26 additions & 15 deletions pages/api/playerExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { PlayerCardDataType } from "../../src/coh3/coh3-types";
import { json2csvAsync } from "json-2-csv";
import { NextApiRequest, NextApiResponse } from "next";
import { generateCSVObject } from "../../src/players/export";
import { chunk } from "lodash";

const getPlayerInfo = async (profileID: string): Promise<PlayerCardDataType> => {
return processPlayerInfoAPIResponse(await getPlayerCardInfo(profileID));
return processPlayerInfoAPIResponse(await getPlayerCardInfo(profileID, true));
};

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
Expand All @@ -24,38 +25,48 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}

if (typeof profileIDs !== "string") {
return res.status(400).json({ error: "profile id contains invalid params" });
return res.status(400).json({ error: "profile id contains invalid data" });
}
let parsedTypes;
let parsedTypes: ["1v1", "2v2", "3v3", "4v4"];

if (types !== undefined && typeof types !== "string") {
return res.status(400).json({ error: "profile id contains invalid params" });
return res.status(400).json({ error: "types contains invalid data" });
}

if (types !== undefined) {
parsedTypes = JSON.parse(types);
try {
parsedTypes = JSON.parse(types);
} catch (e) {
logger.error(e);
return res.status(400).json({ error: "error parsing the types data" });
}

if (!parsedTypes.every((type) => ["1v1", "2v2", "3v3", "4v4"].includes(type))) {
return res.status(400).json({ error: "parsedTypes contains invalid data" });
}
}

const arrayOfIds = JSON.parse(profileIDs);
const arrayOfIds: Array<string> = JSON.parse(profileIDs);
logger.log(`Going to parse ${arrayOfIds.length} ids`);
logger.log(`List of IDs ${arrayOfIds}`);
if (arrayOfIds.length > 100) {
return res.status(500).json({ error: "Too many records requested" });
if (arrayOfIds.length > 50) {
return res.status(400).json({ error: "Too many records requested" });
}

const finalArray = [];

for (const profileId of arrayOfIds) {
const playerInfo = await getPlayerInfo(profileId);
const playerInfoAsCSVObject = generateCSVObject(
playerInfo,
profileId,
parsedTypes || undefined,
for (const singleChunk of chunk(arrayOfIds, 2)) {
const playerInfoPromises = singleChunk.map((profileId) => getPlayerInfo(profileId));
const playerInfoArray = await Promise.all(playerInfoPromises);
const playerInfoAsCSVObjects = playerInfoArray.map((playerInfo, index) =>
generateCSVObject(playerInfo, singleChunk[index], parsedTypes || undefined),
);
finalArray.push(playerInfoAsCSVObject);
finalArray.push(...playerInfoAsCSVObjects);
}

res
.status(200)
.setHeader("Cache-Control", "public, max-age=60")
.setHeader("content-type", "text/csv")
.send(await json2csvAsync(finalArray, {}));
} catch (e) {
Expand Down
16 changes: 13 additions & 3 deletions pages/other/open-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Container, Text, Title, Anchor, Code, Spoiler } from "@mantine/core";
import React, { useEffect } from "react";
import config from "../../config";
import { AnalyticsOpenDataPageView } from "../../src/firebase/analytics";
import { generateKeywordsString } from "../../src/head-utils";

const codeForLeaderboards = `https://storage.coh3stats.com/leaderboards/{unixTimeStamp}/{unixTimeStamp}_{mode}_{faction}.json
Expand Down Expand Up @@ -121,24 +122,33 @@ interface ProcessedMatchHistoryItem {
// Check https://github.com/cohstats/coh3-stats/blob/master/src/coh3/coh3-raw-data.ts for additional details
`;

const keywords = generateKeywordsString([
"coh3 data",
"coh3 matches",
"download matches",
"coh3 match api",
"coh3 leaderboards api",
]);

/**
* This is example page you can find it by going on ur /example
* @constructor
*/
const About: NextPage = () => {
const OpenData: NextPage = () => {
useEffect(() => {
AnalyticsOpenDataPageView();
}, []);

return (
<div>
{/*This is custom HEAD overwrites the default one*/}
<Head>
<title>COH3 Stats - Open Data</title>
<meta
name="description"
content="COH3 Stats are open sourcing a leaderboards and match data. Find more details on the page how to download them."
/>
<meta name="keywords" content={keywords} />
<meta property="og:image" content={`/logo/android-icon-192x192.png`} />
</Head>
<>
<Container size={"md"}>
Expand Down Expand Up @@ -259,4 +269,4 @@ const About: NextPage = () => {
);
};

export default About;
export default OpenData;
102 changes: 102 additions & 0 deletions pages/other/player-export.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { NextPage } from "next";
import React, { useEffect } from "react";
import { AnalyticsPlayerExportPageView } from "../../src/firebase/analytics";
import Head from "next/head";
import { Anchor, Code, Container, Space, Text, Title } from "@mantine/core";
import { generateKeywordsString } from "../../src/head-utils";

const exampleOutput = `alias,relic_id,steam_id,1v1_axis_elo,1v1_allies_elo,german_1v1_rank,german_1v1_elo,german_1v1_total,american_1v1_rank,american_1v1_elo,american_1v1_total,dak_1v1_rank,dak_1v1_elo,dak_1v1_total,british_1v1_rank,british_1v1_elo,british_1v1_total
Isildur,3705,76561198018614046,1432,1475,-1,1432,23,-1,1307,14,-1,1417,13,-1,1475,34
Rei,871,76561198404414770,1547,1607,43,1547,106,33,1602,139,26,1510,76,-1,1607,87
jibber,6219,76561198090318538,1717,1679,10,1717,229,21,1657,187,-1,1594,232,-1,1679,100
Luvnest,108833,76561197982704567,1543,1634,45,1543,146,37,1593,101,67,1400,85,18,1634,97
elpern,61495,76561198019498694,1681,1717,-1,1681,139,-1,1699,178,-1,1611,100,-1,1717,156
IncaUna,1287,76561198152399446,1557,1691,-1,1557,24,-1,1218,5,-1,1510,46,-1,1691,157`;

const keywords = generateKeywordsString(["coh3 players", "export", "csv"]);

const PlayerExport: NextPage = () => {
useEffect(() => {
AnalyticsPlayerExportPageView();
}, []);

return (
<>
<Head>
<title>COH3 Stats - Player Export in CSV</title>
<meta
name="description"
content="COH3 Stats player export - export player leaderboards in CSV format."
/>
<meta name="keywords" content={keywords} />
<meta property="og:image" content={`/logo/android-icon-192x192.png`} />
</Head>
<>
<Container size={"md"}>
<Title order={1} pt="md">
COH3 Stats - Player Export in CSV
</Title>
<Title order={4} pt="md">
API for tournament organizers.
</Title>
<Text pt="md">
This API gives you access to players leaderboard stats in CSV format. Which you can
easily import in Excel sheet.
</Text>
<Space />
<Text pt="md">
Export the data via this link:
<Code
block
>{`https://coh3stats.com/api/playerExport?types=["1v1"]&profileIDs=[3705,871,6219,108833,61495,1287]`}</Code>
</Text>
<Text pt="md">
Parameter <Code>types</Code> can be <Code>{`["1v1", "2v2", "3v3", "4v4"]`}</Code>.
<br />
Parameter <Code>profileIDs</Code> is an array of Relic profile IDs. You can find them
on COH3 Stats player cards.
</Text>
<Space h={"xl"} />
<Text>
Example how to import the data into Google Sheets is{" "}
<Anchor
href={
"https://docs.google.com/spreadsheets/d/1K3aEixDvrnEB_Xdvwjx_NsTRqsWv2Kp4-ZSGwxbY7c8/edit?usp=sharing"
}
target={"_blank"}
>
here
</Anchor>
.
</Text>
<Space />
<Title order={4} pt="md">
Example output:
</Title>
<Code block>{exampleOutput}</Code>

<Space h={"xl"} />
<Text>
In case you need the data for your tournament in different format or you have any
other questions, let us know on Discord.
</Text>
<Text fs="italic">
Shout-out for coh3stats.com at your tournament would be awesome. Thank you
</Text>
<Space h={"xl"} />
<Text>
Please note that this API is designed for use in tournaments or with a limited number
of players.
<Text span fw={500}>
{" "}
It is not intended for programmatic use.
</Text>{" "}
For any API collaboration head over to our Discord.
</Text>
</Container>
</>
</>
);
};

export default PlayerExport;
4 changes: 4 additions & 0 deletions src/firebase/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export const AnalyticsOpenDataPageView = (): void => {
logFBEvent("open_data_view");
};

export const AnalyticsPlayerExportPageView = (): void => {
logFBEvent("player_export_view");
};

export const SearchPageView = (): void => {
logFBEvent("search_view");
};
Expand Down
4 changes: 4 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export const getOpenDataRoute = () => {
return encodeURI(`/other/open-data`);
};

export const getPlayerExportRoute = () => {
return encodeURI(`/other/player-export`);
};

export const getRankingTiersRoute = () => {
return encodeURI(`/other/ranking-tiers`);
};

0 comments on commit 77989fb

Please sign in to comment.