Skip to content

Commit

Permalink
feat: pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
rharkor committed Jul 26, 2023
1 parent d846875 commit 0b3d7b5
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 84 deletions.
20 changes: 13 additions & 7 deletions src/app/api/sessions/active/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server"
import { requireAuthApi } from "@/components/auth/require-auth"
import { IJsonApiResponse, parseJsonApiQuery } from "@/lib/json-api"
import { getJsonApiSkip, getJsonApiSort, getJsonApiTake, IJsonApiResponse, parseJsonApiQuery } from "@/lib/json-api"
import { prisma } from "@/lib/prisma"

export async function GET(request: Request) {
Expand All @@ -9,25 +9,31 @@ export async function GET(request: Request) {

const { searchParams } = new URL(request.url)
const query = parseJsonApiQuery(searchParams)
console.log(query)

const activeSessions = await prisma.session.findMany({
where: {
userId: session.user.id,
},
skip: getJsonApiSkip(query),
take: getJsonApiTake(query),
orderBy: getJsonApiSort(query),
})

const total = await prisma.session.count({
where: {
userId: session.user.id,
},
})

const response: IJsonApiResponse<(typeof activeSessions)[number]> = {
data: activeSessions,
meta: {
total: activeSessions.length,
page: 1,
perPage: 10,
totalPages: 1,
page: query.page,
perPage: query.perPage,
totalPages: Math.ceil(total / query.perPage),
},
}

NextResponse.next

return NextResponse.json(response)
}
19 changes: 10 additions & 9 deletions src/components/profile/sessions/sessions-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function SessionsTable() {
`/api/sessions/active?${jsonApiQuery({
page: currentPage,
perPage: itemsPerPage,
sort: ["-lastUsedAt"],
})}`
),
() => {
Expand Down Expand Up @@ -91,15 +92,15 @@ export default function SessionsTable() {
<div className="mt-4 flex flex-col space-y-4">
<AlertDialog>
{sessions ? rows : skelRows}
{sessions && (sessions.meta.totalPages > 1 || itemsPerPageInitial !== itemsPerPage) && (
<Pagination
currentPage={sessions.meta.page}
totalPages={sessions.meta.totalPages}
setCurrentPage={setCurrentPage}
itemsPerPage={sessions.meta.perPage}
setItemsPerPage={setItemsPerPage}
/>
)}
<Pagination
show={sessions && (sessions.meta.totalPages > 1 || itemsPerPageInitial !== itemsPerPage)}
currentNumberOfItems={sessions?.data?.length ?? 0}
currentPage={sessions?.meta.page}
totalPages={sessions?.meta.totalPages}
setCurrentPage={setCurrentPage}
itemsPerPage={sessions?.meta.perPage}
setItemsPerPage={setItemsPerPage}
/>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
Expand Down
146 changes: 81 additions & 65 deletions src/components/ui/pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,105 @@
"use client"

import { Dispatch, SetStateAction } from "react"
import { logger } from "@/lib/logger"
import { cn } from "@/lib/utils"
import { Button } from "./button"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
import { Icons } from "../icons"

interface PaginationProps {
currentPage: number
setCurrentPage: Dispatch<SetStateAction<number>>
totalPages: number
currentNumberOfItems?: number
currentPage?: number
setCurrentPage?: Dispatch<SetStateAction<number>>
totalPages?: number
itemsPerPage?: number
setItemsPerPage?: Dispatch<SetStateAction<number>>
show?: boolean
}

export default function Pagination({
currentNumberOfItems,
currentPage,
setCurrentPage,
totalPages,
itemsPerPage,
setItemsPerPage,
show = true,
}: PaginationProps) {
//? If there are no items, and we're not on the first page, go to the first page
if (currentNumberOfItems === 0 && (currentPage ?? 1) > 1) {
logger.debug("Pagination: No items, going to first page")
setCurrentPage?.(1)
}

return (
<div className="ml-auto flex items-center justify-between px-2">
<div className="flex items-center space-x-6 lg:space-x-8">
{itemsPerPage && setItemsPerPage && (
<div className={cn("grid grid-rows-[0fr] transition-all duration-300 ease-out", show && "grid-rows-[1fr]")}>
<div className="ml-auto flex items-center justify-between overflow-hidden px-2">
<div className="flex items-center space-x-6 lg:space-x-8">
{itemsPerPage && setItemsPerPage && (
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={itemsPerPage.toString()}
onValueChange={(value) => {
setItemsPerPage(parseInt(value, 10))
setCurrentPage?.(1)
}}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={itemsPerPage.toString()} />
</SelectTrigger>
<SelectContent side="top">
{[5, 10, 20].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {currentPage} of {totalPages}
</div>
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={itemsPerPage.toString()}
onValueChange={(value) => {
setItemsPerPage(parseInt(value, 10))
setCurrentPage(1)
}}
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => setCurrentPage?.(1)}
disabled={currentPage === 1}
>
<span className="sr-only">Go to first page</span>
<Icons.doubleArrowRight className="h-4 w-4 rotate-180" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => setCurrentPage?.((prev) => prev - 1)}
disabled={currentPage === 1}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={itemsPerPage.toString()} />
</SelectTrigger>
<SelectContent side="top">
{[5, 10, 20].map((pageSize) => (
<SelectItem key={pageSize} value={pageSize.toString()}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="sr-only">Go to previous page</span>
<Icons.chevronRight className="h-4 w-4 rotate-180" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => setCurrentPage?.((prev) => prev + 1)}
disabled={currentPage === totalPages}
>
<span className="sr-only">Go to next page</span>
<Icons.chevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => setCurrentPage?.(totalPages ?? 1)}
disabled={currentPage === totalPages}
>
<span className="sr-only">Go to last page</span>
<Icons.doubleArrowRight className="h-4 w-4" />
</Button>
</div>
)}
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {currentPage} of {totalPages}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
>
<span className="sr-only">Go to first page</span>
<Icons.doubleArrowRight className="h-4 w-4 rotate-180" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => setCurrentPage((prev) => prev - 1)}
disabled={currentPage === 1}
>
<span className="sr-only">Go to previous page</span>
<Icons.chevronRight className="h-4 w-4 rotate-180" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => setCurrentPage((prev) => prev + 1)}
disabled={currentPage === totalPages}
>
<span className="sr-only">Go to next page</span>
<Icons.chevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages}
>
<span className="sr-only">Go to last page</span>
<Icons.doubleArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</div>
Expand Down
34 changes: 31 additions & 3 deletions src/lib/json-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,17 @@ export const jsonApiQuery = (query: IJsonApiQuery) => {
return searchParams
}

export const parseJsonApiQuery = (query: URLSearchParams | string): IJsonApiQuery => {
export const jsonApiDefaults = {
page: 1,
perPage: 10,
}

export type IJsonApiQueryWithDefaults = IJsonApiQuery & typeof jsonApiDefaults

export const parseJsonApiQuery = (
query: URLSearchParams | string,
defaults = jsonApiDefaults
): IJsonApiQueryWithDefaults => {
const searchParams = new URLSearchParams(query)

const page = searchParams.get("page")
Expand All @@ -87,8 +97,8 @@ export const parseJsonApiQuery = (query: URLSearchParams | string): IJsonApiQuer
const fields = searchParams.get("fields")

return {
page: page ? parseInt(page) : undefined,
perPage: perPage ? parseInt(perPage) : undefined,
page: page ? parseInt(page) : defaults.page,
perPage: perPage ? parseInt(perPage) : defaults.perPage,
sort: sort ? sort.split(",") : undefined,
filter: filter
? filter.split(",").map((filter) => {
Expand All @@ -100,3 +110,21 @@ export const parseJsonApiQuery = (query: URLSearchParams | string): IJsonApiQuer
fields: fields ? fields.split(",") : undefined,
}
}

export const getJsonApiSkip = ({ page, perPage }: { page: number; perPage: number }) => {
return perPage * (page - 1)
}

export const getJsonApiTake = ({ perPage }: { perPage: number }) => {
return perPage
}

export const getJsonApiSort = ({ sort }: { sort?: string[] }) => {
if (!sort) return undefined

return sort.map((sort) => {
const direction = sort[0] === "-" ? "desc" : "asc"
const field = sort.replace(/^-/, "")
return { [field]: direction }
})
}

0 comments on commit 0b3d7b5

Please sign in to comment.