feat(ssr): server side render some things to speed up page loads
All checks were successful
deploy / deploy (push) Successful in 1m12s
All checks were successful
deploy / deploy (push) Successful in 1m12s
This commit is contained in:
parent
3a6312510a
commit
c251239e45
@ -60,41 +60,41 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withBundleAnalyzer(nextConfig);
|
||||
// module.exports = withBundleAnalyzer(nextConfig);
|
||||
|
||||
// // Injected content via Sentry wizard below
|
||||
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
// const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
module.exports,
|
||||
{
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
// module.exports = withSentryConfig(
|
||||
// module.exports,
|
||||
// {
|
||||
// // For all available options, see:
|
||||
// // https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
// Suppresses source map uploading logs during build
|
||||
silent: true,
|
||||
org: "sentry",
|
||||
project: "scoresaber-reloaded",
|
||||
url: "https://sentry.fascinated.cc/",
|
||||
},
|
||||
{
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
// // Suppresses source map uploading logs during build
|
||||
// silent: true,
|
||||
// org: "sentry",
|
||||
// project: "scoresaber-reloaded",
|
||||
// url: "https://sentry.fascinated.cc/",
|
||||
// },
|
||||
// {
|
||||
// // For all available options, see:
|
||||
// // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: false,
|
||||
// // Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
// widenClientFileUpload: false,
|
||||
|
||||
// Transpiles SDK to be compatible with IE11 (increases bundle size)
|
||||
transpileClientSDK: false,
|
||||
// // Transpiles SDK to be compatible with IE11 (increases bundle size)
|
||||
// transpileClientSDK: false,
|
||||
|
||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
tunnelRoute: "/monitoring",
|
||||
// // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
// tunnelRoute: "/monitoring",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
// // Hides source maps from generated client bundles
|
||||
// hideSourceMaps: true,
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
},
|
||||
);
|
||||
// // Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
// disableLogger: true,
|
||||
// },
|
||||
// );
|
||||
|
@ -47,6 +47,24 @@ export async function generateMetadata({
|
||||
};
|
||||
}
|
||||
|
||||
export default function Player({ params: { id, sort, page } }: Props) {
|
||||
return <PlayerPage id={id} sort={sort} page={page} />;
|
||||
/**
|
||||
* Gets the player's data on the server side.
|
||||
*
|
||||
* @param id the player's id
|
||||
* @returns the player's data
|
||||
*/
|
||||
async function getData(id: string) {
|
||||
const response = await ScoreSaberAPI.fetchPlayerData(id);
|
||||
return {
|
||||
data: response,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Player({ params: { id, sort, page } }: Props) {
|
||||
const { data } = await getData(id);
|
||||
if (!data) {
|
||||
return <div>Player not found</div>;
|
||||
}
|
||||
|
||||
return <PlayerPage player={data} sort={sort} page={page} />;
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import Card from "@/components/Card";
|
||||
import Container from "@/components/Container";
|
||||
import Error from "@/components/Error";
|
||||
import GlobalRanking from "@/components/GlobalRanking";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -9,6 +13,43 @@ type Props = {
|
||||
params: { page: string; country: string };
|
||||
};
|
||||
|
||||
export default function RankingGlobal({ params: { page, country } }: Props) {
|
||||
return <GlobalRanking page={Number(page)} country={country} />;
|
||||
async function getData(page: number, country: string) {
|
||||
const response = await ScoreSaberAPI.fetchTopPlayers(page, country);
|
||||
return {
|
||||
data: response,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RankingGlobal({
|
||||
params: { page, country },
|
||||
}: Props) {
|
||||
const { data } = await getData(Number(page), country);
|
||||
if (!data) {
|
||||
return (
|
||||
<main>
|
||||
<Container>
|
||||
<Card outerClassName="mt-2" className="mt-2">
|
||||
<div className="p-3 text-center">
|
||||
<div role="status">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Error errorMessage="Unable to find this page or country" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalRanking
|
||||
pageInfo={{
|
||||
page: Number(page),
|
||||
totalPages: data.pageInfo.totalPages,
|
||||
}}
|
||||
players={data.players}
|
||||
country={country}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import Card from "@/components/Card";
|
||||
import Container from "@/components/Container";
|
||||
import Error from "@/components/Error";
|
||||
import GlobalRanking from "@/components/GlobalRanking";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -9,6 +13,40 @@ type Props = {
|
||||
params: { page: string };
|
||||
};
|
||||
|
||||
export default function RankingGlobal({ params: { page } }: Props) {
|
||||
return <GlobalRanking page={Number(page)} />;
|
||||
async function getData(page: number) {
|
||||
const response = await ScoreSaberAPI.fetchTopPlayers(page);
|
||||
return {
|
||||
data: response,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RankingGlobal({ params: { page } }: Props) {
|
||||
const { data } = await getData(Number(page));
|
||||
if (!data) {
|
||||
return (
|
||||
<main>
|
||||
<Container>
|
||||
<Card outerClassName="mt-2" className="mt-2">
|
||||
<div className="p-3 text-center">
|
||||
<div role="status">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Error errorMessage="Unable to find this page" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalRanking
|
||||
pageInfo={{
|
||||
page: Number(page),
|
||||
totalPages: data.pageInfo.totalPages,
|
||||
}}
|
||||
players={data.players}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,163 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import { normalizedRegionName } from "@/utils/utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Card from "./Card";
|
||||
import Container from "./Container";
|
||||
import CountyFlag from "./CountryFlag";
|
||||
import Pagination from "./Pagination";
|
||||
import Spinner from "./Spinner";
|
||||
import PlayerRanking from "./player/PlayerRanking";
|
||||
import PlayerRankingMobile from "./player/PlayerRankingMobile";
|
||||
import { Separator } from "./ui/separator";
|
||||
|
||||
const Error = dynamic(() => import("@/components/Error"));
|
||||
|
||||
type PageInfo = {
|
||||
loading: boolean;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
players: ScoresaberPlayer[];
|
||||
};
|
||||
|
||||
type GlobalRankingProps = {
|
||||
page: number;
|
||||
players: ScoresaberPlayer[];
|
||||
country?: string;
|
||||
pageInfo: {
|
||||
page: number;
|
||||
totalPages: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function GlobalRanking({ page, country }: GlobalRankingProps) {
|
||||
const router = useRouter();
|
||||
const searchQuery = useSearchParams();
|
||||
const isMobile = searchQuery.get("mobile") == "true";
|
||||
|
||||
const [error, setError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo>({
|
||||
loading: true,
|
||||
page: page,
|
||||
totalPages: 1,
|
||||
players: [],
|
||||
});
|
||||
|
||||
const updatePage = useCallback(
|
||||
(page: any) => {
|
||||
const windowSize = document.documentElement.clientWidth;
|
||||
if (windowSize < 768 && !isMobile) {
|
||||
router.push(`/ranking/global/${page}?mobile=true`);
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Switching page to", page);
|
||||
ScoreSaberAPI.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,
|
||||
});
|
||||
window.history.pushState(
|
||||
{},
|
||||
"",
|
||||
country
|
||||
? `/ranking/country/${country}/${page}`
|
||||
: `/ranking/global/${page}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
[country, isMobile, pageInfo, router],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageInfo.loading || error) return;
|
||||
|
||||
updatePage(pageInfo.page);
|
||||
}, [error, country, updatePage, pageInfo.page, pageInfo.loading]);
|
||||
|
||||
if (pageInfo.loading || error) {
|
||||
return (
|
||||
<main>
|
||||
<Container>
|
||||
<Card outerClassName="mt-2" className="mt-2">
|
||||
<div className="p-3 text-center">
|
||||
<div role="status">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
{error && <Error errorMessage={errorMessage} />}
|
||||
{!error && <Spinner />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const players = pageInfo.players;
|
||||
|
||||
export default function GlobalRanking({
|
||||
players,
|
||||
country,
|
||||
pageInfo,
|
||||
}: GlobalRankingProps) {
|
||||
return (
|
||||
<main>
|
||||
<Container>
|
||||
<Card outerClassName="mt-2" className="mt-2">
|
||||
{pageInfo.loading ? (
|
||||
<div className="flex justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
{country && (
|
||||
<CountyFlag countryCode={country} className="!h-8 !w-8" />
|
||||
)}
|
||||
<p>
|
||||
You are viewing{" "}
|
||||
{country
|
||||
? "scores from " +
|
||||
normalizedRegionName(country.toUpperCase())
|
||||
: "Global scores"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{!isMobile && (
|
||||
<table className="table w-full table-auto border-spacing-2 border-none text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2">Rank</th>
|
||||
<th className="px-4 py-2">Profile</th>
|
||||
<th className="px-4 py-2">Performance Points</th>
|
||||
<th className="px-4 py-2">Total Plays</th>
|
||||
<th className="px-4 py-2">Total Ranked Plays</th>
|
||||
<th className="px-4 py-2">Avg Ranked Accuracy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="border-none">
|
||||
{players.map((player) => (
|
||||
<tr key={player.rank} className="border-b border-border">
|
||||
<PlayerRanking
|
||||
showCountryFlag={country ? false : true}
|
||||
player={player}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
{country && (
|
||||
<CountyFlag countryCode={country} className="!h-8 !w-8" />
|
||||
)}
|
||||
<p>
|
||||
You are viewing{" "}
|
||||
{country
|
||||
? "scores from " + normalizedRegionName(country.toUpperCase())
|
||||
: "Global scores"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<table className="table w-full table-auto border-spacing-2 border-none text-left">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2">Rank</th>
|
||||
<th className="px-4 py-2">Profile</th>
|
||||
<th className="px-4 py-2">Performance Points</th>
|
||||
<th className="px-4 py-2">Total Plays</th>
|
||||
<th className="px-4 py-2">Total Ranked Plays</th>
|
||||
<th className="px-4 py-2">Avg Ranked Accuracy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="border-none">
|
||||
{players.map((player) => (
|
||||
<tr key={player.rank} className="border-b border-border">
|
||||
<PlayerRanking
|
||||
showCountryFlag={country ? false : true}
|
||||
player={player}
|
||||
/>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{/*
|
||||
{isMobile && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{players.map((player) => (
|
||||
@ -171,22 +78,18 @@ export default function GlobalRanking({ page, country }: GlobalRankingProps) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<div className="pt-3">
|
||||
<Pagination
|
||||
currentPage={pageInfo.page}
|
||||
totalPages={pageInfo.totalPages}
|
||||
onPageChange={(page) => {
|
||||
updatePage(page);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Pagination */}
|
||||
<div className="flex w-full flex-row justify-center">
|
||||
<div className="pt-3">
|
||||
<Pagination
|
||||
currentPage={pageInfo.page}
|
||||
totalPages={pageInfo.totalPages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
</main>
|
||||
|
@ -2,15 +2,17 @@ import {
|
||||
ArrowUturnLeftIcon,
|
||||
ArrowUturnRightIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
type PaginationProps = {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (pageNumber: number) => void;
|
||||
useHref?: boolean;
|
||||
onPageChange?: (pageNumber: number) => void;
|
||||
};
|
||||
|
||||
export default function Pagination(props: PaginationProps) {
|
||||
const { currentPage, totalPages, onPageChange } = props;
|
||||
const { currentPage, totalPages, useHref, onPageChange } = props;
|
||||
|
||||
// Calculate the range of page numbers to display
|
||||
const rangeStart = Math.max(1, currentPage - 2);
|
||||
@ -28,26 +30,48 @@ export default function Pagination(props: PaginationProps) {
|
||||
<ul className="flex items-center gap-2">
|
||||
{currentPage > 1 && (
|
||||
<li className="rounded-md bg-neutral-700 hover:opacity-80">
|
||||
<button
|
||||
className="px-3 py-1"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
aria-label={`Page ${currentPage - 1} (previous page)`}
|
||||
>
|
||||
<ArrowUturnLeftIcon width={20} height={20} />
|
||||
</button>
|
||||
{useHref ? (
|
||||
<Link href={`?page=${currentPage - 1}`}>
|
||||
<a
|
||||
className="px-3 py-1"
|
||||
aria-label={`Page ${currentPage - 1} (previous page)`}
|
||||
>
|
||||
<ArrowUturnLeftIcon width={20} height={20} />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className="px-3 py-1"
|
||||
onClick={() => onPageChange && onPageChange(currentPage - 1)}
|
||||
aria-label={`Page ${currentPage - 1} (previous page)`}
|
||||
>
|
||||
<ArrowUturnLeftIcon width={20} height={20} />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
{currentPage !== 1 && currentPage - 2 > 1 && (
|
||||
<>
|
||||
<li>
|
||||
<button
|
||||
className="rounded-md bg-neutral-700 px-3 py-1 hover:opacity-80"
|
||||
onClick={() => onPageChange(1)}
|
||||
aria-label="Page 1 (first page)"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
{useHref ? (
|
||||
<Link href={`?page=1`}>
|
||||
<a
|
||||
className="rounded-md bg-neutral-700 px-3 py-1 hover:opacity-80"
|
||||
aria-label="Page 1 (first page)"
|
||||
>
|
||||
1
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className="rounded-md bg-neutral-700 px-3 py-1 hover:opacity-80"
|
||||
onClick={() => onPageChange && onPageChange(1)}
|
||||
aria-label="Page 1 (first page)"
|
||||
>
|
||||
1
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<p>...</p>
|
||||
@ -57,17 +81,32 @@ export default function Pagination(props: PaginationProps) {
|
||||
|
||||
{pageNumbers.map((pageNumber) => (
|
||||
<li key={pageNumber}>
|
||||
<button
|
||||
className={`rounded-md px-3 py-1 ${
|
||||
pageNumber === currentPage
|
||||
? "bg-blue-500 text-primary"
|
||||
: "bg-neutral-700 hover:opacity-80"
|
||||
}`}
|
||||
onClick={() => onPageChange(pageNumber)}
|
||||
aria-label={`Page ${pageNumber}`}
|
||||
>
|
||||
{pageNumber}
|
||||
</button>
|
||||
{useHref ? (
|
||||
<Link href={`?page=${pageNumber}`}>
|
||||
<a
|
||||
className={`rounded-md px-3 py-1 ${
|
||||
pageNumber === currentPage
|
||||
? "bg-blue-500 text-primary"
|
||||
: "bg-neutral-700 hover:opacity-80"
|
||||
}`}
|
||||
aria-label={`Page ${pageNumber}`}
|
||||
>
|
||||
{pageNumber}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className={`rounded-md px-3 py-1 ${
|
||||
pageNumber === currentPage
|
||||
? "bg-blue-500 text-primary"
|
||||
: "bg-neutral-700 hover:opacity-80"
|
||||
}`}
|
||||
onClick={() => onPageChange && onPageChange(pageNumber)}
|
||||
aria-label={`Page ${pageNumber}`}
|
||||
>
|
||||
{pageNumber}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@ -78,26 +117,48 @@ export default function Pagination(props: PaginationProps) {
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button
|
||||
className="rounded-md bg-neutral-700 px-3 py-1 hover:opacity-80"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
aria-label={`Page ${totalPages} (last page)`}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
{useHref ? (
|
||||
<Link href={`?page=${totalPages}`}>
|
||||
<a
|
||||
className="rounded-md bg-neutral-700 px-3 py-1 hover:opacity-80"
|
||||
aria-label={`Page ${totalPages} (last page)`}
|
||||
>
|
||||
{totalPages}
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className="rounded-md bg-neutral-700 px-3 py-1 hover:opacity-80"
|
||||
onClick={() => onPageChange && onPageChange(totalPages)}
|
||||
aria-label={`Page ${totalPages} (last page)`}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentPage < totalPages && (
|
||||
<li className="rounded-md bg-neutral-700 hover:opacity-80">
|
||||
<button
|
||||
className="px-3 py-1"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
aria-label={`Page ${currentPage + 1} (next page)`}
|
||||
>
|
||||
<ArrowUturnRightIcon width={20} height={20} />
|
||||
</button>
|
||||
{useHref ? (
|
||||
<Link href={`?page=${currentPage + 1}`}>
|
||||
<a
|
||||
className="px-3 py-1"
|
||||
aria-label={`Page ${currentPage + 1} (next page)`}
|
||||
>
|
||||
<ArrowUturnRightIcon width={20} height={20} />
|
||||
</a>
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
className="px-3 py-1"
|
||||
onClick={() => onPageChange && onPageChange(currentPage + 1)}
|
||||
aria-label={`Page ${currentPage + 1} (next page)`}
|
||||
>
|
||||
<ArrowUturnRightIcon width={20} height={20} />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { formatNumber } from "@/utils/numberUtils";
|
||||
import {
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||
import { useSettingsStore } from "@/store/settingsStore";
|
||||
@ -11,7 +13,7 @@ import {
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { useStore } from "zustand";
|
||||
import Avatar from "../Avatar";
|
||||
@ -27,6 +29,7 @@ type PlayerInfoProps = {
|
||||
};
|
||||
|
||||
export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const playerId = playerData.id;
|
||||
const settingsStore = useStore(useSettingsStore, (store) => store);
|
||||
const playerScoreStore = useStore(useScoresaberScoresStore, (store) => store);
|
||||
@ -36,6 +39,10 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||
|
||||
const toastId: any = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
async function claimProfile() {
|
||||
settingsStore?.setProfile(playerData);
|
||||
addProfile(false);
|
||||
@ -122,32 +129,36 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||
|
||||
{/* Settings Buttons */}
|
||||
<div className="absolute right-3 top-20 flex flex-col justify-end gap-2 md:relative md:right-0 md:top-0 md:mt-2 md:flex-row md:justify-center">
|
||||
{!isOwnProfile && (
|
||||
<Button
|
||||
onClick={claimProfile}
|
||||
tooltip={<p>Set as your Profile</p>}
|
||||
icon={<HomeIcon width={24} height={24} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isOwnProfile && (
|
||||
{mounted && (
|
||||
<>
|
||||
{!settingsStore?.isFriend(playerId) && (
|
||||
{!isOwnProfile && (
|
||||
<Button
|
||||
onClick={addFriend}
|
||||
tooltip={<p>Add as Friend</p>}
|
||||
icon={<UserIcon width={24} height={24} />}
|
||||
color="bg-green-500"
|
||||
onClick={claimProfile}
|
||||
tooltip={<p>Set as your Profile</p>}
|
||||
icon={<HomeIcon width={24} height={24} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{settingsStore.isFriend(playerId) && (
|
||||
<Button
|
||||
onClick={removeFriend}
|
||||
tooltip={<p>Remove Friend</p>}
|
||||
icon={<XMarkIcon width={24} height={24} />}
|
||||
color="bg-red-500"
|
||||
/>
|
||||
{!isOwnProfile && (
|
||||
<>
|
||||
{!settingsStore?.isFriend(playerId) && (
|
||||
<Button
|
||||
onClick={addFriend}
|
||||
tooltip={<p>Add as Friend</p>}
|
||||
icon={<UserIcon width={24} height={24} />}
|
||||
color="bg-green-500"
|
||||
/>
|
||||
)}
|
||||
|
||||
{settingsStore.isFriend(playerId) && (
|
||||
<Button
|
||||
onClick={removeFriend}
|
||||
tooltip={<p>Remove Friend</p>}
|
||||
icon={<XMarkIcon width={24} height={24} />}
|
||||
color="bg-red-500"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@ -165,7 +176,10 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||
|
||||
<a
|
||||
className="flex transform-gpu items-center gap-1 transition-all hover:text-blue-500"
|
||||
href={`/ranking/global/${Math.round(playerData.rank / 50)}`}
|
||||
href={`/ranking/global/${Math.max(
|
||||
Math.round(playerData.rank / 50),
|
||||
1,
|
||||
)}`}
|
||||
>
|
||||
<p>#{formatNumber(playerData.rank)}</p>
|
||||
</a>
|
||||
@ -175,8 +189,9 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||
<div className="text-gray-300">
|
||||
<a
|
||||
className="flex transform-gpu items-center gap-1 transition-all hover:text-blue-500"
|
||||
href={`/ranking/country/${playerData.country}/${Math.round(
|
||||
playerData.countryRank / 50,
|
||||
href={`/ranking/country/${playerData.country}/${Math.max(
|
||||
Math.round(playerData.countryRank / 50),
|
||||
1,
|
||||
)}`}
|
||||
>
|
||||
<CountyFlag
|
||||
|
@ -1,94 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import Card from "@/components/Card";
|
||||
import Container from "@/components/Container";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import Scores from "@/components/player/Scores";
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { SortTypes } from "@/types/SortTypes";
|
||||
import { ScoreSaberAPI } from "@/utils/scoresaber/api";
|
||||
import clsx from "clsx";
|
||||
import dynamic from "next/dynamic";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
|
||||
import PlayerChart from "./PlayerChart";
|
||||
import PlayerInfo from "./PlayerInfo";
|
||||
|
||||
const Error = dynamic(() => import("@/components/Error"));
|
||||
|
||||
type PlayerInfo = {
|
||||
loading: boolean;
|
||||
player: ScoresaberPlayer | undefined;
|
||||
};
|
||||
|
||||
type PlayerPageProps = {
|
||||
id: string;
|
||||
player: ScoresaberPlayer;
|
||||
sort: string;
|
||||
page: string;
|
||||
};
|
||||
|
||||
const DEFAULT_SORT_TYPE = SortTypes.top;
|
||||
|
||||
export default function PlayerPage({ id, sort, page }: PlayerPageProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const [player, setPlayer] = useState<PlayerInfo>({
|
||||
loading: true,
|
||||
player: undefined,
|
||||
});
|
||||
|
||||
export default function PlayerPage({ player, sort, page }: PlayerPageProps) {
|
||||
const sortType = SortTypes[sort] || DEFAULT_SORT_TYPE;
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (error || !player.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScoreSaberAPI.fetchPlayerData(id).then((playerResponse) => {
|
||||
if (!playerResponse) {
|
||||
setError(true);
|
||||
setErrorMessage("Failed to fetch player. Is the ID correct?");
|
||||
setPlayer({ ...player, loading: false });
|
||||
return;
|
||||
}
|
||||
setPlayer({ ...player, player: playerResponse, loading: false });
|
||||
});
|
||||
}, [error, mounted, id, player]);
|
||||
|
||||
if (player.loading || error || !player.player) {
|
||||
return (
|
||||
<main>
|
||||
<Container>
|
||||
<Card outerClassName="mt-2" className="mt-2">
|
||||
<div className="p-3 text-center">
|
||||
<div role="status">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
{error && <Error errorMessage={errorMessage} />}
|
||||
{!error && <Spinner />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Container>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const playerData = player.player;
|
||||
const badges = playerData.badges;
|
||||
const badges = player.badges;
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Container>
|
||||
<PlayerInfo playerData={playerData} />
|
||||
<PlayerInfo playerData={player} />
|
||||
{/* Chart */}
|
||||
<Card outerClassName="mt-2">
|
||||
{/* Badges */}
|
||||
@ -117,14 +59,10 @@ export default function PlayerPage({ id, sort, page }: PlayerPageProps) {
|
||||
})}
|
||||
</div>
|
||||
<div className="h-[320px] w-full">
|
||||
<PlayerChart scoresaber={playerData} />
|
||||
<PlayerChart scoresaber={player} />
|
||||
</div>
|
||||
</Card>
|
||||
<Scores
|
||||
playerData={playerData}
|
||||
page={Number(page)}
|
||||
sortType={sortType}
|
||||
/>
|
||||
<Scores playerData={player} page={Number(page)} sortType={sortType} />
|
||||
</Container>
|
||||
</main>
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||
import { useSettingsStore } from "@/store/settingsStore";
|
||||
|
@ -23,7 +23,12 @@ export class FetchQueue {
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
next: {
|
||||
revalidate: 300, // 5 minutes
|
||||
},
|
||||
});
|
||||
if (response.status === 429) {
|
||||
const hasRetryAfterHeader = response.headers.has("retry-after");
|
||||
let retryAfter = hasRetryAfterHeader
|
||||
|
Loading…
x
Reference in New Issue
Block a user