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); }}