feat: pagination

This commit is contained in:
trafficlunar 2025-04-13 20:00:33 +01:00
parent 3f4a757bb1
commit cada748f2d
3 changed files with 99 additions and 2 deletions

View file

@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams)); const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams));
if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 }); if (!parsed.success) return NextResponse.json({ error: parsed.error.errors[0].message }, { status: 400 });
const { q: query, sort, tags, userId, page = 1, limit = 20 } = parsed.data; const { q: query, sort, tags, userId, page = 1, limit = 24 } = parsed.data;
const where: Prisma.MiiWhereInput = { const where: Prisma.MiiWhereInput = {
// Searching // Searching
@ -92,7 +92,7 @@ export async function GET(request: NextRequest) {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
const [totalCount, filteredCount, list] = await Promise.all([ const [totalCount, filteredCount, list] = await Promise.all([
prisma.mii.count({ where: userId ? { userId } : {} }), prisma.mii.count({ where: { ...where, userId } }),
prisma.mii.count({ where, skip, take: limit }), prisma.mii.count({ where, skip, take: limit }),
prisma.mii.findMany({ where, orderBy, select, skip: (page - 1) * limit, take: limit }), prisma.mii.findMany({ where, orderBy, select, skip: (page - 1) * limit, take: limit }),
]); ]);
@ -100,6 +100,7 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
total: totalCount, total: totalCount,
filtered: filteredCount, filtered: filteredCount,
lastPage: Math.ceil(totalCount / limit),
miis: list.map(({ _count, likedBy, ...rest }) => ({ miis: list.map(({ _count, likedBy, ...rest }) => ({
...rest, ...rest,
likes: _count.likedBy, likes: _count.likedBy,

View file

@ -8,6 +8,7 @@ import SortSelect from "./sort-select";
import Carousel from "../carousel"; import Carousel from "../carousel";
import LikeButton from "../like-button"; import LikeButton from "../like-button";
import FilterSelect from "./filter-select"; import FilterSelect from "./filter-select";
import Pagination from "./pagination";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -18,6 +19,7 @@ interface Props {
interface ApiResponse { interface ApiResponse {
total: number; total: number;
filtered: number; filtered: number;
lastPage: number;
miis: { miis: {
id: number; id: number;
user?: { user?: {
@ -131,6 +133,8 @@ export default function MiiList({ isLoggedIn, userId }: Props) {
) : ( ) : (
<>{error && <p className="text-xl text-red-400 font-semibold text-center mt-10">Error: {error}</p>}</> <>{error && <p className="text-xl text-red-400 font-semibold text-center mt-10">Error: {error}</p>}</>
)} )}
{data && <Pagination lastPage={data.lastPage} />}
</div> </div>
); );
} }

View file

@ -0,0 +1,92 @@
"use client";
import { redirect, useSearchParams } from "next/navigation";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { Icon } from "@iconify/react";
interface Props {
lastPage: number;
}
export default function Pagination({ lastPage }: Props) {
const searchParams = useSearchParams();
const page = Number(searchParams.get("page") ?? 1);
const numbers = useMemo(() => {
const result = [];
// Always show 5 pages, centering around the current page when possible
const start = Math.max(1, Math.min(page - 2, lastPage - 4));
const end = Math.min(lastPage, start + 4);
for (let i = start; i <= end; i++) result.push(i);
return result;
}, [page, lastPage]);
return (
<div className="flex justify-center items-center w-full mt-8">
{/* Go to first page */}
<Link
href={page === 1 ? "#" : "/?page=1"}
aria-disabled={page === 1}
tabIndex={page === 1 ? -1 : undefined}
className={`pill button !bg-orange-100 !p-0.5 aspect-square text-2xl ${
page === 1 ? "pointer-events-none opacity-50" : "hover:!bg-orange-400"
}`}
>
<Icon icon="stash:chevron-double-left" />
</Link>
{/* Previous page */}
<Link
href={page === 1 ? "#" : `/?page=${page - 1}`}
aria-disabled={page === 1}
tabIndex={page === 1 ? -1 : undefined}
className={`pill !bg-orange-100 !p-0.5 aspect-square text-2xl ${page === 1 ? "pointer-events-none opacity-50" : "hover:!bg-orange-400"}`}
>
<Icon icon="stash:chevron-left" />
</Link>
{/* Page numbers */}
<div className="flex mx-2">
{numbers.map((number) => (
<Link
key={number}
href={`/?page=${number}`}
aria-current={number === page ? "page" : undefined}
className={`pill !p-0 w-8 h-8 text-center !rounded-md ${number == page ? "!bg-orange-400" : "!bg-orange-100 hover:!bg-orange-400"}`}
>
{number}
</Link>
))}
</div>
{/* Next page */}
<Link
href={page === lastPage ? "#" : `/?page=${page + 1}`}
aria-disabled={page === lastPage}
tabIndex={page === lastPage ? -1 : undefined}
className={`pill button !bg-orange-100 !p-0.5 aspect-square text-2xl ${
page === lastPage ? "pointer-events-none opacity-50" : "hover:!bg-orange-400"
}`}
>
<Icon icon="stash:chevron-right" />
</Link>
{/* Go to last page */}
<Link
href={page === lastPage ? "#" : `/?page=${lastPage}`}
aria-disabled={page === lastPage}
tabIndex={page === lastPage ? -1 : undefined}
className={`pill button !bg-orange-100 !p-0.5 aspect-square text-2xl ${
page === lastPage ? "pointer-events-none opacity-50" : "hover:!bg-orange-400"
}`}
>
<Icon icon="stash:chevron-double-right" />
</Link>
</div>
);
}