diff --git a/apps/web/app/(app)/environments/[environmentId]/people/page.tsx b/apps/web/app/(app)/environments/[environmentId]/people/page.tsx index 25d2dd64c4..bcd734c513 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/page.tsx @@ -2,17 +2,35 @@ export const revalidate = REVALIDATION_INTERVAL; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import { truncateMiddle } from "@/lib/utils"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; -import { getPeople } from "@formbricks/lib/services/person"; +import { PEOPLE_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getPeople, getPeopleCount } from "@formbricks/lib/services/person"; import { TPerson } from "@formbricks/types/v1/people"; -import { PersonAvatar } from "@formbricks/ui"; +import { Pagination, PersonAvatar } from "@formbricks/ui"; import Link from "next/link"; const getAttributeValue = (person: TPerson, attributeName: string) => person.attributes[attributeName]?.toString(); -export default async function PeoplePage({ params }) { - const people = await getPeople(params.environmentId); +export default async function PeoplePage({ + params, + searchParams, +}: { + params: { environmentId: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}) { + const pageNumber = searchParams.page ? parseInt(searchParams.page as string) : 1; + const totalPeople = await getPeopleCount(params.environmentId); + const maxPageNumber = Math.ceil(totalPeople / PEOPLE_PER_PAGE); + let hidePagination = false; + + let people: TPerson[] = []; + + if (pageNumber < 1 || pageNumber > maxPageNumber) { + people = []; + hidePagination = true; + } else { + people = await getPeople(params.environmentId, pageNumber); + } return ( <> @@ -64,6 +82,14 @@ export default async function PeoplePage({ params }) { ))} )} + {hidePagination ? null : ( + + )} ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/people/pagination.tsx b/apps/web/app/(app)/environments/[environmentId]/people/pagination.tsx new file mode 100644 index 0000000000..f215893e10 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/people/pagination.tsx @@ -0,0 +1,57 @@ +function Pagination({ environmentId, currentPage, totalItems, itemsPerPage }) { + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const previousPageLink = + currentPage === 1 ? "#" : `/environments/${environmentId}/people?page=${currentPage - 1}`; + const nextPageLink = + currentPage === totalPages ? "#" : `/environments/${environmentId}/people?page=${currentPage + 1}`; + + return ( + + ); +} + +export default Pagination; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index fb30af491f..1062751402 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -25,3 +25,5 @@ export const SURVEY_BASE_URL = process.env.NEXT_PUBLIC_SURVEY_BASE_URL export const INTERNAL_SECRET = process.env.INTERNAL_SECRET || ""; export const CRON_SECRET = process.env.CRON_SECRET; export const DEFAULT_BRAND_COLOR = "#64748b"; + +export const PEOPLE_PER_PAGE = 50; diff --git a/packages/lib/services/person.ts b/packages/lib/services/person.ts index e3aa9d6c34..ba85839826 100644 --- a/packages/lib/services/person.ts +++ b/packages/lib/services/person.ts @@ -2,11 +2,12 @@ import "server-only"; import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/v1/environment"; -import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; +import { DatabaseError } from "@formbricks/types/v1/errors"; import { TPerson } from "@formbricks/types/v1/people"; import { Prisma } from "@prisma/client"; import { revalidateTag, unstable_cache } from "next/cache"; import { cache } from "react"; +import { PEOPLE_PER_PAGE } from "../constants"; import { validateInputs } from "../utils/validate"; import { getAttributeClassByName } from "./attributeClass"; @@ -100,20 +101,24 @@ export const getPersonCached = async (personId: string) => } )(); -export const getPeople = cache(async (environmentId: string): Promise => { +export const getPeople = cache(async (environmentId: string, page: number = 1): Promise => { validateInputs([environmentId, ZId]); try { - const personsPrisma = await prisma.person.findMany({ + const itemsPerPage = PEOPLE_PER_PAGE; + const people = await prisma.person.findMany({ where: { environmentId: environmentId, }, select: selectPerson, + take: itemsPerPage, + skip: itemsPerPage * (page - 1), }); - if (!personsPrisma) { - throw new ResourceNotFoundError("Persons", "All Persons"); + + if (!people || people.length === 0) { + return []; } - const transformedPeople: TPerson[] = personsPrisma + const transformedPeople: TPerson[] = people .map(transformPrismaPerson) .filter((person: TPerson | null): person is TPerson => person !== null); @@ -127,6 +132,24 @@ export const getPeople = cache(async (environmentId: string): Promise } }); +export const getPeopleCount = cache(async (environmentId: string): Promise => { + validateInputs([environmentId, ZId]); + try { + const totalCount = await prisma.person.count({ + where: { + environmentId: environmentId, + }, + }); + return totalCount; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } +}); + export const createPerson = async (environmentId: string): Promise => { validateInputs([environmentId, ZId]); try { diff --git a/packages/ui/components/Pagination.tsx b/packages/ui/components/Pagination.tsx new file mode 100644 index 0000000000..20cddba4de --- /dev/null +++ b/packages/ui/components/Pagination.tsx @@ -0,0 +1,72 @@ +function Pagination({ + baseUrl, + currentPage, + totalItems, + itemsPerPage, +}: { + baseUrl: string; + currentPage: number; + totalItems: number; + itemsPerPage: number; +}) { + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const previousPageLink = currentPage === 1 ? "#" : `${baseUrl}?page=${currentPage - 1}`; + const nextPageLink = currentPage === totalPages ? "#" : `${baseUrl}?page=${currentPage + 1}`; + + const getDisplayedPages = () => { + if (totalPages <= 20) { + return Array.from({ length: totalPages }, (_, idx) => idx + 1); + } else { + let range = [currentPage - 2, currentPage - 1, currentPage, currentPage + 1, currentPage + 2]; + return [1, ...range.filter((n) => n > 1 && n < totalPages), totalPages]; + } + }; + + return ( + + ); +} + +export { Pagination }; diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 7b56e83371..3ef9e9603f 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -50,6 +50,7 @@ export { export { ErrorComponent } from "./components/ErrorComponent"; export { Input } from "./components/Input"; export { Label } from "./components/Label"; +export { Pagination } from "./components/Pagination"; export { PasswordInput } from "./components/PasswordInput"; export { Popover, PopoverContent, PopoverTrigger } from "./components/Popover"; export { HalfCircle, ProgressBar } from "./components/ProgressBar";