diff --git a/src/app/(pages)/api/proxy/route.ts b/src/app/(pages)/api/proxy/route.ts
new file mode 100644
index 0000000..edbec74
--- /dev/null
+++ b/src/app/(pages)/api/proxy/route.ts
@@ -0,0 +1,47 @@
+import { validateUrl } from "@/common/utils";
+import ky from "ky";
+import { NextRequest, NextResponse } from "next/server";
+
+export async function GET(request: NextRequest) {
+ const url = request.nextUrl.searchParams.get("url");
+ if (url == null) {
+ return NextResponse.json({ error: "Missing URL. ?url=" }, { status: 400 });
+ }
+
+ if (!validateUrl(url)) {
+ return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
+ }
+
+ try {
+ const response = await ky.get(url, {
+ next: {
+ revalidate: 30, // 30 seconds
+ },
+ });
+ const { status, headers } = response;
+ if (
+ !headers.has("content-type") ||
+ (headers.has("content-type") && !headers.get("content-type")?.includes("application/json"))
+ ) {
+ return NextResponse.json({
+ error: "We only support proxying JSON responses",
+ });
+ }
+
+ const body = await response.json();
+ return NextResponse.json(body, {
+ status: status,
+ });
+ } catch (err) {
+ console.error(`Error fetching data from ${url}:`, err);
+ return NextResponse.json(
+ { error: "Failed to proxy this request." },
+ {
+ status: 500,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ },
+ }
+ );
+ }
+}
diff --git a/src/common/data-fetcher/data-fetcher.ts b/src/common/data-fetcher/data-fetcher.ts
index a136ef6..3b393d8 100644
--- a/src/common/data-fetcher/data-fetcher.ts
+++ b/src/common/data-fetcher/data-fetcher.ts
@@ -28,6 +28,7 @@ export default class DataFetcher {
*/
private buildRequestUrl(useProxy: boolean, url: string): string {
return (useProxy ? "https://proxy.fascinated.cc/" : "") + url;
+ // return (useProxy ? config.siteUrl + "/api/proxy?url=" : "") + url;
}
/**
diff --git a/src/common/utils.ts b/src/common/utils.ts
index bd0c391..12ed29d 100644
--- a/src/common/utils.ts
+++ b/src/common/utils.ts
@@ -1,6 +1,21 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+/**
+ * Validates if the url is valid
+ *
+ * @param url the url to validate
+ * @returns true if the url is valid, false otherwise
+ */
+export function validateUrl(url: string) {
+ try {
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
}
diff --git a/src/components/input/pagination.tsx b/src/components/input/pagination.tsx
index d6be48b..beb5bc9 100644
--- a/src/components/input/pagination.tsx
+++ b/src/components/input/pagination.tsx
@@ -1,3 +1,5 @@
+import { ArrowPathIcon } from "@heroicons/react/24/solid";
+import clsx from "clsx";
import { useEffect, useState } from "react";
import {
PaginationContent,
@@ -9,6 +11,19 @@ import {
Pagination as ShadCnPagination,
} from "../ui/pagination";
+type PaginationItemWrapperProps = {
+ isLoadingPage: boolean;
+ children: React.ReactNode;
+};
+
+function PaginationItemWrapper({ isLoadingPage, children }: PaginationItemWrapperProps) {
+ return (
+
+ {children}
+
+ );
+}
+
type Props = {
/**
* If true, the pagination will be rendered as a mobile-friendly pagination.
@@ -25,14 +40,20 @@ type Props = {
*/
totalPages: number;
+ /**
+ * The page to show a loading icon on.
+ */
+ loadingPage: number | undefined;
+
/**
* Callback function that is called when the user clicks on a page number.
*/
onPageChange: (page: number) => void;
};
-export default function Pagination({ mobilePagination, page, totalPages, onPageChange }: Props) {
+export default function Pagination({ mobilePagination, page, totalPages, loadingPage, onPageChange }: Props) {
totalPages = Math.round(totalPages);
+ const isLoading = loadingPage !== undefined;
const [currentPage, setCurrentPage] = useState(page);
useEffect(() => {
@@ -40,7 +61,7 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
}, [page]);
const handlePageChange = (newPage: number) => {
- if (newPage < 1 || newPage > totalPages || newPage == currentPage) {
+ if (newPage < 1 || newPage > totalPages || newPage == currentPage || isLoading) {
return;
}
@@ -62,12 +83,12 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
if (startPage > 1 && !mobilePagination) {
pageNumbers.push(
<>
-
+
handlePageChange(1)}>1
-
-
+
+
-
+
>
);
}
@@ -75,11 +96,11 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
// Generate page numbers between startPage and endPage for desktop view
for (let i = startPage; i <= endPage; i++) {
pageNumbers.push(
-
+
handlePageChange(i)}>
- {i}
+ {loadingPage === i ? : i}
-
+
);
}
@@ -90,28 +111,28 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
{/* Previous button for mobile and desktop */}
-
+
handlePageChange(currentPage - 1)} />
-
+
{renderPageNumbers()}
{/* For desktop, show ellipsis and link to the last page */}
{!mobilePagination && currentPage < totalPages && (
<>
-
+
-
-
+
+
handlePageChange(totalPages)}>{totalPages}
-
+
>
)}
{/* Next button for mobile and desktop */}
-
+
handlePageChange(currentPage + 1)} />
-
+
);
diff --git a/src/components/player/player-scores.tsx b/src/components/player/player-scores.tsx
index 628f79a..5b26944 100644
--- a/src/components/player/player-scores.tsx
+++ b/src/components/player/player-scores.tsx
@@ -25,7 +25,7 @@ export default function PlayerScores({ player, sort, page }: Props) {
const [currentPage, setCurrentPage] = useState(page);
const [previousScores, setPreviousScores] = useState();
- const { data, isError, refetch } = useQuery({
+ const { data, isError, isLoading, refetch } = useQuery({
queryKey: ["playerScores", player.id, currentSort, currentPage],
queryFn: () => scoresaberFetcher.lookupPlayerScores(player.id, currentSort, currentPage),
});
@@ -89,6 +89,7 @@ export default function PlayerScores({ player, sort, page }: Props) {
mobilePagination={width < 768}
page={currentPage}
totalPages={Math.ceil(previousScores.metadata.total / previousScores.metadata.itemsPerPage)}
+ loadingPage={isLoading ? currentPage : undefined}
onPageChange={(newPage) => {
setCurrentPage(newPage);
}}