Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start": "next start -p $PORT",
"lint": "next lint",
"check-types": "tsc --noEmit",
"test": "tsx --test src/components/locations-pagination-cache.test.ts src/components/locations-pagination-navigation.test.ts src/lib/locations-pagination-request.test.ts",
"format": "prettier . --write",
"check-format": "prettier --check .",
"all-checks": "npm run check-format && npm run check-types && npm run lint",
Expand Down
143 changes: 143 additions & 0 deletions src/app/api/locations-pagination/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) 2024 Streetlives, Inc.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import {
REQUIREMENT_PARAM,
RESOURCE_ROUTES,
SHELTER_PARAM_SINGLE_VALUE,
SHELTER_PARAM_YOUTH_VALUE,
SORT_BY_QUERY_PARAM,
SearchParams,
SubRouteParams,
parseCategoryFromRoute,
parseRequest,
} from "@/components/common";
import {
getFullLocationData,
getTaxonomies,
map_gogetta_to_yourpeer,
} from "@/components/streetlives-api-service";
import {
parseLocationsBackgroundPageNumber,
parseLocationsBackgroundPageSize,
} from "@/lib/locations-pagination-request";

function toSearchParamsObject(
urlSearchParams: URLSearchParams,
excludedKeys: Set<string>,
): SearchParams {
const entries = new Map<string, string[]>();

urlSearchParams.forEach((value, key) => {
if (excludedKeys.has(key)) {
return;
}

const existing = entries.get(key);
if (existing) {
existing.push(value);
return;
}

entries.set(key, [value]);
});

return Object.fromEntries(
Array.from(entries.entries()).map(([key, values]) => [
key,
values.length === 1 ? values[0] : values,
]),
);
}

export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url);
const route = requestUrl.searchParams.get("route");

if (!route || !RESOURCE_ROUTES.includes(route)) {
return NextResponse.json(
{ error: "Expected a valid route query parameter." },
{ status: 400 },
);
}

const locationSlugOrPersonalCareSubCategory =
requestUrl.searchParams.get("locationSlugOrPersonalCareSubCategory") ||
undefined;

const params: SubRouteParams | { route: string } =
locationSlugOrPersonalCareSubCategory
? {
route,
locationSlugOrPersonalCareSubCategory,
}
: { route };

const page = parseLocationsBackgroundPageNumber(
requestUrl.searchParams.get("pageNumber"),
);
if (page === null) {
return NextResponse.json(
{
error: "pageNumber exceeds the supported background pagination range.",
},
{ status: 400 },
);
}
const pageSize = parseLocationsBackgroundPageSize(
requestUrl.searchParams.get("pageSize"),
);

const searchParams = toSearchParamsObject(
requestUrl.searchParams,
new Set([
"route",
"locationSlugOrPersonalCareSubCategory",
"page",
"pageNumber",
"pageSize",
]),
);

const parsedSearchParams = parseRequest({
params,
searchParams,
cookies: await cookies(),
});
const category = parseCategoryFromRoute(route);
const taxonomiesResults = await getTaxonomies(category, parsedSearchParams);

const locationParams = {
...parsedSearchParams,
...parsedSearchParams[REQUIREMENT_PARAM],
...taxonomiesResults,
page,
pageSize,
sortBy: parsedSearchParams[SORT_BY_QUERY_PARAM],
...(locationSlugOrPersonalCareSubCategory === SHELTER_PARAM_YOUTH_VALUE && {
ageMin: 16,
ageMax: 24,
}),
...(locationSlugOrPersonalCareSubCategory ===
SHELTER_PARAM_SINGLE_VALUE && {
ageMin: 18,
ageMax: 99,
}),
};

const { locations, resultCount } = await getFullLocationData(locationParams);

return NextResponse.json({
pageNumber: page,
pageSize,
resultCount,
locations: locations.map((location) =>
map_gogetta_to_yourpeer(location, false),
),
});
}
58 changes: 36 additions & 22 deletions src/components/locations-container-pager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,47 @@ import {
ChevronsLeft,
ChevronsRight,
} from "lucide-react";
import React from "react";

export function LocationsContainerPager({
resultCount,
numberOfPages,
currentPage,
onPageChange,
}: {
resultCount: number;
numberOfPages: number;
currentPage: number;
onPageChange?: (pageNumber: number) => void;
}) {
const pathname = usePathname();
const searchParams = useSearchParams();
const hasPreviousPage = currentPage > 0;
const hasNextPage = currentPage < numberOfPages;

const firstPageHref = hasPreviousPage
? getFirstPageHref(pathname, searchParams)
: undefined;
const previousPageHref = hasPreviousPage
? getUrlToNextOrPreviousPage(pathname, searchParams, false)
: undefined;
const nextPageHref = hasNextPage
? getUrlToNextOrPreviousPage(pathname, searchParams, true)
: undefined;
const lastPageHref = hasNextPage
? getLastPageHref(pathname, searchParams, numberOfPages)
: undefined;

function handlePageChange(
event: React.MouseEvent<HTMLAnchorElement>,
pageNumber: number,
) {
if (!onPageChange || pageNumber < 0 || pageNumber > numberOfPages) {
return;
}

event.preventDefault();
onPageChange(pageNumber);
}

return (
<div className="p-6 border-t border-neutral-100 mb-14 md:mb-0">
<div className="flex items-center justify-between">
Expand All @@ -42,11 +68,8 @@ export function LocationsContainerPager({
className={`text-dark inline-flex space-x-1 disabled:text-muted ${
!hasPreviousPage ? "text-muted cursor-not-allowed" : ""
}`}
href={
hasPreviousPage
? getFirstPageHref(pathname, searchParams)
: undefined
}
href={firstPageHref}
onClick={(event) => handlePageChange(event, 0)}
>
<ChevronsLeft className="w-6 h-6" />
</a>
Expand All @@ -55,11 +78,8 @@ export function LocationsContainerPager({
className={`text-dark inline-flex space-x-1 disabled:text-muted ${
!hasPreviousPage ? "text-muted cursor-not-allowed" : ""
}`}
href={
hasPreviousPage
? getUrlToNextOrPreviousPage(pathname, searchParams, false)
: undefined
}
href={previousPageHref}
onClick={(event) => handlePageChange(event, currentPage - 1)}
>
<ChevronLeft className="w-6 h-6" />
<TranslatableText text="Previous" />
Expand All @@ -77,11 +97,8 @@ export function LocationsContainerPager({
className={`inline-flex space-x-1 disabled:text-muted ${
hasNextPage ? "text-dark" : "text-muted cursor-not-allowed"
}`}
href={
hasNextPage
? getUrlToNextOrPreviousPage(pathname, searchParams, true)
: undefined
}
href={nextPageHref}
onClick={(event) => handlePageChange(event, currentPage + 1)}
>
<TranslatableText text="Next" />
<ChevronRight className="w-6 h-6" />
Expand All @@ -91,11 +108,8 @@ export function LocationsContainerPager({
className={`inline-flex space-x-1 disabled:text-muted ${
hasNextPage ? "text-dark" : "text-muted cursor-not-allowed"
}`}
href={
hasNextPage
? getLastPageHref(pathname, searchParams, numberOfPages)
: undefined
}
href={lastPageHref}
onClick={(event) => handlePageChange(event, numberOfPages)}
>
<ChevronsRight className="w-6 h-6" />
</a>
Expand Down
Loading