add loading indicator to the pagination
All checks were successful
Deploy SSR / deploy (push) Successful in 1m37s
All checks were successful
Deploy SSR / deploy (push) Successful in 1m37s
This commit is contained in:
parent
99174e6299
commit
f0dfbe78ea
47
src/app/(pages)/api/proxy/route.ts
Normal file
47
src/app/(pages)/api/proxy/route.ts
Normal file
@ -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": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ export default class DataFetcher {
|
|||||||
*/
|
*/
|
||||||
private buildRequestUrl(useProxy: boolean, url: string): string {
|
private buildRequestUrl(useProxy: boolean, url: string): string {
|
||||||
return (useProxy ? "https://proxy.fascinated.cc/" : "") + url;
|
return (useProxy ? "https://proxy.fascinated.cc/" : "") + url;
|
||||||
|
// return (useProxy ? config.siteUrl + "/api/proxy?url=" : "") + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,21 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||||
|
import clsx from "clsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
PaginationContent,
|
PaginationContent,
|
||||||
@ -9,6 +11,19 @@ import {
|
|||||||
Pagination as ShadCnPagination,
|
Pagination as ShadCnPagination,
|
||||||
} from "../ui/pagination";
|
} from "../ui/pagination";
|
||||||
|
|
||||||
|
type PaginationItemWrapperProps = {
|
||||||
|
isLoadingPage: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function PaginationItemWrapper({ isLoadingPage, children }: PaginationItemWrapperProps) {
|
||||||
|
return (
|
||||||
|
<PaginationItem className={clsx(isLoadingPage ? "cursor-not-allowed" : "cursor-pointer")}>
|
||||||
|
{children}
|
||||||
|
</PaginationItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/**
|
/**
|
||||||
* If true, the pagination will be rendered as a mobile-friendly pagination.
|
* If true, the pagination will be rendered as a mobile-friendly pagination.
|
||||||
@ -25,14 +40,20 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
totalPages: number;
|
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.
|
* Callback function that is called when the user clicks on a page number.
|
||||||
*/
|
*/
|
||||||
onPageChange: (page: number) => void;
|
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);
|
totalPages = Math.round(totalPages);
|
||||||
|
const isLoading = loadingPage !== undefined;
|
||||||
const [currentPage, setCurrentPage] = useState(page);
|
const [currentPage, setCurrentPage] = useState(page);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -40,7 +61,7 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
|
|||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
if (newPage < 1 || newPage > totalPages || newPage == currentPage) {
|
if (newPage < 1 || newPage > totalPages || newPage == currentPage || isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,12 +83,12 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
|
|||||||
if (startPage > 1 && !mobilePagination) {
|
if (startPage > 1 && !mobilePagination) {
|
||||||
pageNumbers.push(
|
pageNumbers.push(
|
||||||
<>
|
<>
|
||||||
<PaginationItem key="start" className="cursor-pointer">
|
<PaginationItemWrapper key="start" isLoadingPage={isLoading}>
|
||||||
<PaginationLink onClick={() => handlePageChange(1)}>1</PaginationLink>
|
<PaginationLink onClick={() => handlePageChange(1)}>1</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
<PaginationItem key="ellipsis-start" className="cursor-pointer">
|
<PaginationItemWrapper key="ellipsis-start" isLoadingPage={isLoading}>
|
||||||
<PaginationEllipsis />
|
<PaginationEllipsis />
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -75,11 +96,11 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
|
|||||||
// Generate page numbers between startPage and endPage for desktop view
|
// Generate page numbers between startPage and endPage for desktop view
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
pageNumbers.push(
|
pageNumbers.push(
|
||||||
<PaginationItem key={i} className="cursor-pointer">
|
<PaginationItemWrapper key={i} isLoadingPage={isLoading}>
|
||||||
<PaginationLink isActive={i === currentPage} onClick={() => handlePageChange(i)}>
|
<PaginationLink isActive={i === currentPage} onClick={() => handlePageChange(i)}>
|
||||||
{i}
|
{loadingPage === i ? <ArrowPathIcon className="w-4 h-4 animate-spin" /> : i}
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,28 +111,28 @@ export default function Pagination({ mobilePagination, page, totalPages, onPageC
|
|||||||
<ShadCnPagination className="select-none">
|
<ShadCnPagination className="select-none">
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
{/* Previous button for mobile and desktop */}
|
{/* Previous button for mobile and desktop */}
|
||||||
<PaginationItem className="cursor-pointer">
|
<PaginationItemWrapper isLoadingPage={isLoading}>
|
||||||
<PaginationPrevious onClick={() => handlePageChange(currentPage - 1)} />
|
<PaginationPrevious onClick={() => handlePageChange(currentPage - 1)} />
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
|
|
||||||
{renderPageNumbers()}
|
{renderPageNumbers()}
|
||||||
|
|
||||||
{/* For desktop, show ellipsis and link to the last page */}
|
{/* For desktop, show ellipsis and link to the last page */}
|
||||||
{!mobilePagination && currentPage < totalPages && (
|
{!mobilePagination && currentPage < totalPages && (
|
||||||
<>
|
<>
|
||||||
<PaginationItem key="ellipsis-end">
|
<PaginationItemWrapper key="ellipsis-end" isLoadingPage={isLoading}>
|
||||||
<PaginationEllipsis className="cursor-default" />
|
<PaginationEllipsis className="cursor-default" />
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
<PaginationItem key="end" className="cursor-pointer">
|
<PaginationItemWrapper key="end" isLoadingPage={isLoading}>
|
||||||
<PaginationLink onClick={() => handlePageChange(totalPages)}>{totalPages}</PaginationLink>
|
<PaginationLink onClick={() => handlePageChange(totalPages)}>{totalPages}</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Next button for mobile and desktop */}
|
{/* Next button for mobile and desktop */}
|
||||||
<PaginationItem className="cursor-pointer">
|
<PaginationItemWrapper isLoadingPage={isLoading}>
|
||||||
<PaginationNext onClick={() => handlePageChange(currentPage + 1)} />
|
<PaginationNext onClick={() => handlePageChange(currentPage + 1)} />
|
||||||
</PaginationItem>
|
</PaginationItemWrapper>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
</ShadCnPagination>
|
</ShadCnPagination>
|
||||||
);
|
);
|
||||||
|
@ -25,7 +25,7 @@ export default function PlayerScores({ player, sort, page }: Props) {
|
|||||||
const [currentPage, setCurrentPage] = useState(page);
|
const [currentPage, setCurrentPage] = useState(page);
|
||||||
const [previousScores, setPreviousScores] = useState<ScoreSaberPlayerScoresPage | undefined>();
|
const [previousScores, setPreviousScores] = useState<ScoreSaberPlayerScoresPage | undefined>();
|
||||||
|
|
||||||
const { data, isError, refetch } = useQuery({
|
const { data, isError, isLoading, refetch } = useQuery({
|
||||||
queryKey: ["playerScores", player.id, currentSort, currentPage],
|
queryKey: ["playerScores", player.id, currentSort, currentPage],
|
||||||
queryFn: () => scoresaberFetcher.lookupPlayerScores(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}
|
mobilePagination={width < 768}
|
||||||
page={currentPage}
|
page={currentPage}
|
||||||
totalPages={Math.ceil(previousScores.metadata.total / previousScores.metadata.itemsPerPage)}
|
totalPages={Math.ceil(previousScores.metadata.total / previousScores.metadata.itemsPerPage)}
|
||||||
|
loadingPage={isLoading ? currentPage : undefined}
|
||||||
onPageChange={(newPage) => {
|
onPageChange={(newPage) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
}}
|
}}
|
||||||
|
Reference in New Issue
Block a user