diff --git a/frontend/src/app/[locale]/search/page.tsx b/frontend/src/app/[locale]/search/page.tsx
index 8a614e793a..c023d09c04 100644
--- a/frontend/src/app/[locale]/search/page.tsx
+++ b/frontend/src/app/[locale]/search/page.tsx
@@ -2,14 +2,15 @@ import BetaAlert from "src/components/BetaAlert";
import Breadcrumbs from "src/components/Breadcrumbs";
import Loading from "src/app/[locale]/search/loading";
import PageSEO from "src/components/PageSEO";
-import SearchResultsList from "src/components/search/SearchResultList";
+import SearchResultsListFetch from "src/components/search/SearchResultsListFetch";
import QueryProvider from "./QueryProvider";
import SearchBar from "src/components/search/SearchBar";
import SearchCallToAction from "src/components/search/SearchCallToAction";
import SearchFilterAccordion from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";
import SearchOpportunityStatus from "src/components/search/SearchOpportunityStatus";
import SearchPagination from "src/components/search/SearchPagination";
-import SearchPaginationLoader from "src/components/search/SearchPaginationLoader";
+import SearchPaginationFetch from "src/components/search/SearchPaginationFetch";
+import SearchResultsHeaderFetch from "src/components/search/SearchResultsHeaderFetch";
import SearchResultsHeader from "src/components/search/SearchResultsHeader";
import withFeatureFlag from "src/hoc/search/withFeatureFlag";
import {
@@ -108,25 +109,48 @@ function Search({ searchParams }: { searchParams: searchParamsTypes }) {
/>
-
+
+ }
+ >
+
+
}
+ fallback={
+
+ }
>
-
}>
-
+
}
+ fallback={
+
+ }
>
-
diff --git a/frontend/src/components/search/SearchPagination.tsx b/frontend/src/components/search/SearchPagination.tsx
index 9aad57328c..03ea044447 100644
--- a/frontend/src/components/search/SearchPagination.tsx
+++ b/frontend/src/components/search/SearchPagination.tsx
@@ -1,31 +1,63 @@
-"use server";
-import { getSearchFetcher } from "src/services/search/searchfetcher/SearchFetcherUtil";
-import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
-import SearchPaginationItem from "./SearchPaginationItem";
+"use client";
+import { Pagination } from "@trussworks/react-uswds";
+import { QueryContext } from "src/app/[locale]/search/QueryProvider";
+import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater";
+import { useContext } from "react";
+
+export enum PaginationPosition {
+ Top = "topPagination",
+ Bottom = "bottomPagination",
+}
interface SearchPaginationProps {
- searchParams: QueryParamData;
- scroll: boolean;
+ page: number;
+ query: string | null | undefined;
+ total?: number | null;
+ scroll?: boolean;
+ totalResults?: string;
+ loading?: boolean;
}
-export default async function SearchPagination({
- searchParams,
- scroll,
+const MAX_SLOTS = 7;
+
+export default function SearchPagination({
+ page,
+ query,
+ total = null,
+ scroll = false,
+ totalResults = "",
+ loading = false,
}: SearchPaginationProps) {
- const searchFetcher = getSearchFetcher();
- const searchResults = await searchFetcher.fetchOpportunities(searchParams);
- const totalPages = searchResults.pagination_info?.total_pages;
- const totalResults = searchResults.pagination_info?.total_records;
+ const { updateQueryParams } = useSearchParamUpdater();
+ const { updateTotalPages, updateTotalResults } = useContext(QueryContext);
+ const { totalPages } = useContext(QueryContext);
+ // Shows total pages from the query context before it is re-fetched from the API.
+ const pages = total || Number(totalPages);
+
+ const updatePage = (page: number) => {
+ updateTotalPages(String(total));
+ updateTotalResults(totalResults);
+ updateQueryParams(String(page), "page", query, scroll);
+ };
return (
- <>
-
+ updatePage(page + 1)}
+ onClickPrevious={() => updatePage(page > 1 ? page - 1 : 0)}
+ onClickPageNumber={(event: React.MouseEvent, page: number) =>
+ updatePage(page)
+ }
/>
- >
+
);
}
diff --git a/frontend/src/components/search/SearchPaginationFetch.tsx b/frontend/src/components/search/SearchPaginationFetch.tsx
new file mode 100644
index 0000000000..a0193d1f19
--- /dev/null
+++ b/frontend/src/components/search/SearchPaginationFetch.tsx
@@ -0,0 +1,33 @@
+"use server";
+import { getSearchFetcher } from "src/services/search/searchfetcher/SearchFetcherUtil";
+import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
+import SearchPagination from "./SearchPagination";
+
+interface SearchPaginationProps {
+ searchParams: QueryParamData;
+ // Determines whether clicking on pager items causes a scroll to the top of the search
+ // results. Created so the bottom pager can scroll.
+ scroll: boolean;
+}
+
+export default async function SearchPaginationFetch({
+ searchParams,
+ scroll,
+}: SearchPaginationProps) {
+ const searchFetcher = getSearchFetcher();
+ const searchResults = await searchFetcher.fetchOpportunities(searchParams);
+ const totalPages = searchResults.pagination_info?.total_pages;
+ const totalResults = searchResults.pagination_info?.total_records;
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/frontend/src/components/search/SearchPaginationItem.tsx b/frontend/src/components/search/SearchPaginationItem.tsx
deleted file mode 100644
index 763a3e5ea5..0000000000
--- a/frontend/src/components/search/SearchPaginationItem.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-"use client";
-
-import { Pagination } from "@trussworks/react-uswds";
-import { QueryContext } from "src/app/[locale]/search/QueryProvider";
-import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater";
-import { useDebouncedCallback } from "use-debounce";
-import { useContext } from "react";
-
-export enum PaginationPosition {
- Top = "topPagination",
- Bottom = "bottomPagination",
-}
-
-interface SearchPaginationProps {
- total: number;
- page: number;
- query: string | null | undefined;
- scroll: boolean;
- totalResults: string;
-}
-
-const MAX_SLOTS = 7;
-
-export default function SearchPaginationItem({
- total,
- page,
- query,
- scroll,
- totalResults,
-}: SearchPaginationProps) {
- const { updateQueryParams } = useSearchParamUpdater();
- const { updateTotalPages, updateTotalResults } = useContext(QueryContext);
-
- // TODO: determine better state management. The results are grabbed on the server but
- // not available to the client components that aren't suspense wrapped.
- updateTotalResults(totalResults);
- const debouncedUpdate = useDebouncedCallback((page: string) => {
- updateQueryParams(page, "page", query, scroll);
- }, 50);
-
- const updatePage = (page: number) => {
- updateTotalPages(String(total));
- debouncedUpdate(String(page));
- };
-
- return (
- <>
-
updatePage(page + 1)}
- onClickPrevious={() => updatePage(page > 1 ? page - 1 : 0)}
- onClickPageNumber={(event: React.MouseEvent, page: number) =>
- updatePage(page)
- }
- />
- >
- );
-}
diff --git a/frontend/src/components/search/SearchPaginationLoader.tsx b/frontend/src/components/search/SearchPaginationLoader.tsx
deleted file mode 100644
index e754309e0e..0000000000
--- a/frontend/src/components/search/SearchPaginationLoader.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-import { Pagination } from "@trussworks/react-uswds";
-import { QueryContext } from "../../app/[locale]/search/QueryProvider";
-import { useContext } from "react";
-
-interface SearchPaginationProps {
- page: number;
-}
-
-const MAX_SLOTS = 7;
-
-export default function SearchPaginationLoader({
- page,
-}: SearchPaginationProps) {
- const { totalPages } = useContext(QueryContext);
- const total = totalPages === "na" ? MAX_SLOTS : totalPages;
-
- return (
-
- );
-}
diff --git a/frontend/src/components/search/SearchResultsHeader.tsx b/frontend/src/components/search/SearchResultsHeader.tsx
index 00766b97e3..3855a05dbc 100644
--- a/frontend/src/components/search/SearchResultsHeader.tsx
+++ b/frontend/src/components/search/SearchResultsHeader.tsx
@@ -5,18 +5,31 @@ import { useContext } from "react";
export default function SearchResultsHeader({
sortby,
+ totalFetchedResults,
+ queryTerm,
+ loading = false,
}: {
sortby: string | null;
+ totalFetchedResults?: string;
+ queryTerm?: string | null | undefined;
+ loading?: boolean;
}) {
- const { totalResults, queryTerm } = useContext(QueryContext);
-
+ const { totalResults } = useContext(QueryContext);
+ const total = totalFetchedResults || totalResults;
return (
-
- {totalResults.length > 0 && <>{totalResults} Opportunities>}
+
+ {total && <>{total} Opportunities>}
-
+
);
diff --git a/frontend/src/components/search/SearchResultsHeaderFetch.tsx b/frontend/src/components/search/SearchResultsHeaderFetch.tsx
new file mode 100644
index 0000000000..d899124758
--- /dev/null
+++ b/frontend/src/components/search/SearchResultsHeaderFetch.tsx
@@ -0,0 +1,26 @@
+"use server";
+import { getSearchFetcher } from "src/services/search/searchfetcher/SearchFetcherUtil";
+import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
+import SearchResultsHeader from "./SearchResultsHeader";
+
+export default async function SearchResultsHeaderFetch({
+ searchParams,
+ sortby,
+ queryTerm,
+}: {
+ searchParams: QueryParamData;
+ sortby: string | null;
+ queryTerm: string | null | undefined;
+}) {
+ const searchFetcher = getSearchFetcher();
+ const searchResults = await searchFetcher.fetchOpportunities(searchParams);
+ const totalResults = searchResults.pagination_info?.total_records;
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/search/SearchResultList.tsx b/frontend/src/components/search/SearchResultsListFetch.tsx
similarity index 96%
rename from frontend/src/components/search/SearchResultList.tsx
rename to frontend/src/components/search/SearchResultsListFetch.tsx
index 2d7dc973eb..2052a4629a 100644
--- a/frontend/src/components/search/SearchResultList.tsx
+++ b/frontend/src/components/search/SearchResultsListFetch.tsx
@@ -7,7 +7,7 @@ interface ServerPageProps {
searchParams: QueryParamData;
}
-export default async function SearchResultsList({
+export default async function SearchResultsListFetch({
searchParams,
}: ServerPageProps) {
const searchFetcher = getSearchFetcher();
diff --git a/frontend/src/components/search/SearchSortBy.tsx b/frontend/src/components/search/SearchSortBy.tsx
index 074da46f26..da13acd664 100644
--- a/frontend/src/components/search/SearchSortBy.tsx
+++ b/frontend/src/components/search/SearchSortBy.tsx
@@ -1,5 +1,8 @@
+"use client";
import { Select } from "@trussworks/react-uswds";
import { useSearchParamUpdater } from "src/hooks/useSearchParamUpdater";
+import { QueryContext } from "src/app/[locale]/search/QueryProvider";
+import { useContext } from "react";
type SortOption = {
label: string;
@@ -22,13 +25,20 @@ const SORT_OPTIONS: SortOption[] = [
interface SearchSortByProps {
queryTerm: string | null | undefined;
sortby: string | null;
+ totalResults: string;
}
-export default function SearchSortBy({ queryTerm, sortby }: SearchSortByProps) {
+export default function SearchSortBy({
+ queryTerm,
+ sortby,
+ totalResults,
+}: SearchSortByProps) {
const { updateQueryParams } = useSearchParamUpdater();
+ const { updateTotalResults } = useContext(QueryContext);
const handleChange = (event: React.ChangeEvent) => {
const newValue = event.target.value;
+ updateTotalResults(totalResults);
updateQueryParams(newValue, "sortby", queryTerm);
};
diff --git a/frontend/tests/components/search/SearchSortBy.test.tsx b/frontend/tests/components/search/SearchSortBy.test.tsx
index cd9582a636..a977cc9e00 100644
--- a/frontend/tests/components/search/SearchSortBy.test.tsx
+++ b/frontend/tests/components/search/SearchSortBy.test.tsx
@@ -14,7 +14,11 @@ jest.mock("src/hooks/useSearchParamUpdater", () => ({
describe("SearchSortBy", () => {
it("should not have basic accessibility issues", async () => {
const { container } = render(
- ,
+ ,
);
const results = await axe(container);
@@ -22,7 +26,7 @@ describe("SearchSortBy", () => {
});
it("renders correctly with initial query params", () => {
- render();
+ render();
expect(
screen.getByDisplayValue("Posted Date (newest)"),
@@ -34,7 +38,7 @@ describe("SearchSortBy", () => {
const requestSubmitMock = jest.fn();
formElement.requestSubmit = requestSubmitMock;
- render();
+ render();
fireEvent.change(screen.getByRole("combobox"), {
target: { value: "opportunityTitleDesc" },