diff --git a/src/app/ranking/country/[country]/page.tsx b/src/app/ranking/country/[country]/page.tsx new file mode 100644 index 0000000..48a7045 --- /dev/null +++ b/src/app/ranking/country/[country]/page.tsx @@ -0,0 +1,153 @@ +"use client"; + +import Card from "@/components/Card"; +import Container from "@/components/Container"; +import Error from "@/components/Error"; +import Pagination from "@/components/Pagination"; +import { Spinner } from "@/components/Spinner"; +import PlayerRanking from "@/components/player/PlayerRanking"; +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { fetchTopPlayers } from "@/utils/scoresaber/api"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; + +type PageInfo = { + loading: boolean; + page: number; + totalPages: number; + players: ScoresaberPlayer[]; +}; + +export default function RankingCountry({ + params, +}: { + params: { country: string }; +}) { + const searchParams = useSearchParams(); + const router = useRouter(); + + const country = params.country; + + let page; + const pageString = searchParams.get("page"); + if (pageString == null) { + page = 1; + } else { + page = Number.parseInt(pageString) || 1; + } + + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const [pageInfo, setPageInfo] = useState({ + loading: true, + page: page, + totalPages: 1, + players: [], + }); + + const updatePage = useCallback( + (page: any) => { + console.log("Switching page to", page); + fetchTopPlayers(page, country).then((response) => { + if (!response) { + setError(true); + setErrorMessage("No players found"); + setPageInfo({ ...pageInfo, loading: false }); + return; + } + setPageInfo({ + ...pageInfo, + players: response.players, + totalPages: response.pageInfo.totalPages, + loading: false, + page: page, + }); + if (page > 1) { + router.push(`/ranking/country/${country}?page=${page}`, { + scroll: false, + }); + } else { + router.push(`/ranking/country/${country}`, { + scroll: false, + }); + } + }); + }, + [country, pageInfo, router], + ); + + useEffect(() => { + if (!pageInfo.loading || error) return; + + updatePage(pageInfo.page); + }, [error, params.country, updatePage, pageInfo.page, pageInfo.loading]); + + if (pageInfo.loading || error) { + return ( +
+ + +
+
+
+ {error && } + {!error && } +
+
+
+
+
+
+ ); + } + + const players = pageInfo.players; + + return ( +
+ + + {pageInfo.loading ? ( +
+ +
+ ) : ( + + + + + + + + + + + + + {players.map((player) => ( + + + + ))} + +
RankProfilePerformance PointsTotal PlaysTotal Ranked PlaysAvg Ranked Accuracy
+ )} + + {/* Pagination */} +
+
+ { + updatePage(page); + }} + /> +
+
+
+
+
+ ); +} diff --git a/src/app/ranking/global/page.tsx b/src/app/ranking/global/page.tsx index d7c5e50..56a11f0 100644 --- a/src/app/ranking/global/page.tsx +++ b/src/app/ranking/global/page.tsx @@ -1,15 +1,13 @@ "use client"; -import Avatar from "@/components/Avatar"; import Card from "@/components/Card"; import Container from "@/components/Container"; +import Error from "@/components/Error"; import Pagination from "@/components/Pagination"; import { Spinner } from "@/components/Spinner"; +import PlayerRanking from "@/components/player/PlayerRanking"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { formatNumber } from "@/utils/number"; import { fetchTopPlayers } from "@/utils/scoresaber/api"; -import Image from "next/image"; -import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; @@ -20,7 +18,7 @@ type PageInfo = { players: ScoresaberPlayer[]; }; -export default function Player({ params }: { params: { id: string } }) { +export default function RankingGlobal() { const searchParams = useSearchParams(); const router = useRouter(); @@ -77,7 +75,7 @@ export default function Player({ params }: { params: { id: string } }) { if (!pageInfo.loading || error) return; updatePage(pageInfo.page); - }, [error, params.id, updatePage, pageInfo.page, pageInfo.loading]); + }, [error, updatePage, pageInfo.page, pageInfo.loading]); if (pageInfo.loading || error) { return ( @@ -86,20 +84,10 @@ export default function Player({ params }: { params: { id: string } }) {
- {pageInfo.loading && } - - {error && ( -
-

{errorMessage}

- - Sad cat -
- )} +
+ {error && } + {!error && } +
@@ -133,30 +121,7 @@ export default function Player({ params }: { params: { id: string } }) { {players.map((player) => ( - #{formatNumber(player.rank)} - - {" "} - -

{player.name}

- - - {formatNumber(player.pp)}pp - - {formatNumber(player.scoreStats.totalPlayCount)} - - - {formatNumber(player.scoreStats.rankedPlayCount)} - - - {player.scoreStats.averageRankedAccuracy.toFixed(2) + "%"} - + ))} diff --git a/src/components/player/PlayerInfo.tsx b/src/components/player/PlayerInfo.tsx index 79baa16..02ab7e4 100644 --- a/src/components/player/PlayerInfo.tsx +++ b/src/components/player/PlayerInfo.tsx @@ -136,13 +136,20 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) { {/* Country Rank */} -
- -

#{playerData.countryRank}

+ {/* PP */} diff --git a/src/components/player/PlayerRanking.tsx b/src/components/player/PlayerRanking.tsx new file mode 100644 index 0000000..b78cf2a --- /dev/null +++ b/src/components/player/PlayerRanking.tsx @@ -0,0 +1,53 @@ +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { useSettingsStore } from "@/store/settingsStore"; +import { formatNumber } from "@/utils/number"; +import Link from "next/link"; +import ReactCountryFlag from "react-country-flag"; +import { useStore } from "zustand"; +import Avatar from "../Avatar"; + +type PlayerRankingProps = { + player: ScoresaberPlayer; +}; + +export default function PlayerRanking({ player }: PlayerRankingProps) { + const settingsStore = useStore(useSettingsStore, (store) => store); + + return ( + <> + #{formatNumber(player.rank)} + + + + +

+ {player.name} +

+ + + {formatNumber(player.pp)}pp + + {formatNumber(player.scoreStats.totalPlayCount)} + + + {formatNumber(player.scoreStats.rankedPlayCount)} + + + {player.scoreStats.averageRankedAccuracy.toFixed(2) + "%"} + + + ); +} diff --git a/src/utils/scoresaber/api.ts b/src/utils/scoresaber/api.ts index 3a3d1cd..991427d 100644 --- a/src/utils/scoresaber/api.ts +++ b/src/utils/scoresaber/api.ts @@ -14,6 +14,7 @@ const PLAYER_SCORES = API_URL + "/player/{}/scores?limit={}&sort={}&page={}&withMetadata=true"; const GET_PLAYER_DATA_FULL = API_URL + "/player/{}/full"; const GET_PLAYERS_URL = API_URL + "/players?page={}"; +const GET_PLAYERS_BY_COUNTRY_URL = API_URL + "/players?page={}&countries={}"; const SearchType = { RECENT: "recent", @@ -160,9 +161,13 @@ export async function fetchAllScores( * Get the top players * * @param page the page to get the players from + * @param country the country to get the players from * @returns a list of players */ -export async function fetchTopPlayers(page: number = 1): Promise< +export async function fetchTopPlayers( + page: number = 1, + country?: string, +): Promise< | { players: ScoresaberPlayer[]; pageInfo: { @@ -173,9 +178,10 @@ export async function fetchTopPlayers(page: number = 1): Promise< } | undefined > { - const response = await fetchQueue.fetch( - formatString(GET_PLAYERS_URL, true, page), - ); + const url = country + ? formatString(GET_PLAYERS_BY_COUNTRY_URL, true, page, country) + : formatString(GET_PLAYERS_URL, true, page); + const response = await fetchQueue.fetch(url); const json = await response.json(); // Check if there was an error fetching the user data