From ce7eb17242408a6c437d102f6d8bd4a8b591109b Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 19 Oct 2023 15:48:02 +0100 Subject: [PATCH] add basic player view --- package-lock.json | 12 +++ package.json | 1 + src/app/api/player/get/route.ts | 19 ++++ src/app/player/[id]/page.tsx | 144 +++++++++++++++++++++++++++++++ src/components/Label.tsx | 16 ++++ src/schemas/scoresaber/player.ts | 2 +- src/utils/number.ts | 9 ++ src/utils/scoresaber/api.ts | 30 +++++++ yarn.lock | 7 +- 9 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/app/api/player/get/route.ts create mode 100644 src/app/player/[id]/page.tsx create mode 100644 src/components/Label.tsx create mode 100644 src/utils/number.ts diff --git a/package-lock.json b/package-lock.json index 8c14614..cd5d37d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "next": "13.5.5", "node-fetch-cache": "^3.1.3", "react": "^18", + "react-country-flag": "^3.1.0", "react-dom": "^18", "sharp": "^0.32.6", "winston": "^3.11.0" @@ -4602,6 +4603,17 @@ "node": ">=0.10.0" } }, + "node_modules/react-country-flag": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", + "integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index a9a0c90..dcbce93 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "next": "13.5.5", "node-fetch-cache": "^3.1.3", "react": "^18", + "react-country-flag": "^3.1.0", "react-dom": "^18", "sharp": "^0.32.6", "winston": "^3.11.0" diff --git a/src/app/api/player/get/route.ts b/src/app/api/player/get/route.ts new file mode 100644 index 0000000..770c5db --- /dev/null +++ b/src/app/api/player/get/route.ts @@ -0,0 +1,19 @@ +import { getPlayerInfo } from "@/utils/scoresaber/api"; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + if (!id) { + return Response.json({ error: true, message: "No player provided" }); + } + + const player = await getPlayerInfo(id); + if (player == undefined) { + return Response.json({ + error: true, + message: "No players with that ID were found", + }); + } + + return Response.json({ error: false, data: player }); +} diff --git a/src/app/player/[id]/page.tsx b/src/app/player/[id]/page.tsx new file mode 100644 index 0000000..1fcd882 --- /dev/null +++ b/src/app/player/[id]/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import Avatar from "@/components/Avatar"; +import Container from "@/components/Container"; +import Label from "@/components/Label"; +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { formatNumber } from "@/utils/number"; +import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid"; +import Image from "next/image"; +import { useEffect, useState } from "react"; +import ReactCountryFlag from "react-country-flag"; + +// export const metadata: Metadata = { +// title: "todo", +// }; + +export default function Player({ params }: { params: { id: string } }) { + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + const [loading, setLoading] = useState(true); + const [playerData, setPlayerData] = useState( + undefined, + ); + + useEffect(() => { + if (!params.id) { + setError(true); + setLoading(false); + return; + } + if (error || !loading) { + return; + } + fetch("/api/player/get?id=" + params.id).then(async (response) => { + const json = await response.json(); + + if (json.error == true) { + setError(true); + setErrorMessage(json.message); + setLoading(false); + return; + } + + console.log(json); + + setPlayerData(json.data); + setLoading(false); + }); + }, [error, loading, params.id, playerData]); + + if (loading || error || !playerData) { + return ( +
+ +
+
+
+ {loading && ( + <> + + Loading... + + )} + + {error && ( +
+

{errorMessage}

+ + Sad cat +
+ )} +
+
+
+
+
+ ); + } + + return ( +
+ +
+
+ +
+

{playerData.name}

+ +
+ {/* Global Rank */} +
+ +

#{playerData.rank}

+
+ + {/* Country Rank */} +
+ +

#{playerData.countryRank}

+
+ + {/* PP */} +
+

{formatNumber(playerData.pp)}pp

+
+
+ {/* Labels */} +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/Label.tsx b/src/components/Label.tsx new file mode 100644 index 0000000..a579855 --- /dev/null +++ b/src/components/Label.tsx @@ -0,0 +1,16 @@ +type LabelProps = { + title: string; + value: string; +}; + +export default function Label({ title, value }: LabelProps) { + return ( +
+
+

{title}

+
+

{value}

+
+
+ ); +} diff --git a/src/schemas/scoresaber/player.ts b/src/schemas/scoresaber/player.ts index 3f45884..82e894e 100644 --- a/src/schemas/scoresaber/player.ts +++ b/src/schemas/scoresaber/player.ts @@ -12,7 +12,7 @@ export type ScoresaberPlayer = { role: string; badges: ScoresaberBadge[]; histories: string; - scoreStats: ScoresaberScoreStats[]; + scoreStats: ScoresaberScoreStats; permissions: number; banned: boolean; inactive: boolean; diff --git a/src/utils/number.ts b/src/utils/number.ts new file mode 100644 index 0000000..22f512a --- /dev/null +++ b/src/utils/number.ts @@ -0,0 +1,9 @@ +/** + * Formats a number to a string with commas + * + * @param number the number to format + * @returns the formatted number + */ +export function formatNumber(number: number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} diff --git a/src/utils/scoresaber/api.ts b/src/utils/scoresaber/api.ts index 3722240..f9c9461 100644 --- a/src/utils/scoresaber/api.ts +++ b/src/utils/scoresaber/api.ts @@ -17,6 +17,7 @@ const SEARCH_PLAYER_URL = API_URL + "/players?search={}&page=1&withMetadata=false"; const PLAYER_SCORES = API_URL + "/player/{}/scores?limit={}&sort={}&page={}&withMetadata=true"; +const GET_PLAYER_DATA_FULL = API_URL + "/player/{}/full"; const SearchType = { RECENT: "recent", @@ -43,6 +44,35 @@ export async function searchByName( return json.players as ScoresaberPlayer[]; } +/** + * Returns the player info for the provided player id + * + * @param playerId the id of the player + * @returns the player info + */ +export async function getPlayerInfo( + playerId: string, +): Promise { + const response = await fetch(formatString(GET_PLAYER_DATA_FULL, playerId)); + const json = await response.json(); + + // Check if there was an error fetching the user data + if (json.errorMessage) { + return undefined; + } + + return json as ScoresaberPlayer; +} + +/** + * Get the players scores from the given page + * + * @param playerId the id of the player + * @param page the page to get the scores from + * @param searchType the type of search to perform + * @param limit the limit of scores to get + * @returns a list of scores + */ export async function fetchScores( playerId: string, page: number = 1, diff --git a/yarn.lock b/yarn.lock index b505a9e..4baf16e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2608,6 +2608,11 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-country-flag@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz" + integrity sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g== + react-dom@^18, react-dom@^18.0.0, react-dom@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" @@ -2621,7 +2626,7 @@ react-is@^16.13.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react@^16.8.0 || ^17.0.0 || ^18", react@^18, react@^18.0.0, react@^18.2.0, "react@>= 16", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0": +"react@^16.8.0 || ^17.0.0 || ^18", react@^18, react@^18.0.0, react@^18.2.0, "react@>= 16", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==