Skip to content

Commit

Permalink
[Issue# 2260] refactor search error page for maintainability (#2434)
Browse files Browse the repository at this point in the history
* created a layout for the search route
* created a SearchResult component to abstract away from page component
* updated the error page to be up to date with these changes
  • Loading branch information
doug-s-nava authored Oct 10, 2024
1 parent 6403a9e commit ffaaa27
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 227 deletions.
191 changes: 67 additions & 124 deletions frontend/src/app/[locale]/search/error.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
"use client";

// Error components must be Client Components
import QueryProvider from "src/app/[locale]/search/QueryProvider";
import { SEARCH_CRUMBS } from "src/constants/breadcrumbs";
import { QueryParamData } from "src/services/search/searchfetcher/SearchFetcher";
import { ServerSideSearchParams } from "src/types/searchRequestURLTypes";
import { Breakpoints } from "src/types/uiTypes";
import { convertSearchParamsToProperTypes } from "src/utils/search/convertSearchParamsToProperTypes";

import { useTranslations } from "next-intl";
import { useEffect } from "react";

import BetaAlert from "src/components/BetaAlert";
import Breadcrumbs from "src/components/Breadcrumbs";
import PageSEO from "src/components/PageSEO";
import ContentDisplayToggle from "src/components/ContentDisplayToggle";
import SearchErrorAlert from "src/components/search/error/SearchErrorAlert";
import SearchBar from "src/components/search/SearchBar";
import SearchCallToAction from "src/components/search/SearchCallToAction";
import SearchFilterAccordion from "src/components/search/SearchFilterAccordion/SearchFilterAccordion";
import {
agencyOptions,
categoryOptions,
eligibilityOptions,
fundingOptions,
} from "src/components/search/SearchFilterAccordion/SearchFilterOptions";
import SearchOpportunityStatus from "src/components/search/SearchOpportunityStatus";
import SearchResultsHeader from "src/components/search/SearchResultsHeader";
import SearchFilters from "src/components/search/SearchFilters";

interface ErrorProps {
// Next's error boundary also includes a reset function as a prop for retries,
Expand All @@ -31,138 +21,91 @@ interface ErrorProps {

export interface ParsedError {
message: string;
searchInputs: QueryParamData;
searchInputs: ServerSideSearchParams;
status: number;
type: string;
}

function isValidJSON(str: string) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false; // String is not valid JSON
}
}

function createBlankParsedError(): ParsedError {
return {
type: "NetworkError",
searchInputs: {
query: "",
status: "",
fundingInstrument: "",
eligibility: "",
agency: "",
category: "",
sortby: undefined,
page: "1",
actionType: "initialLoad",
},
message: "Invalid error message JSON returned",
status: -1,
};
}

export default function Error({ error }: ErrorProps) {
const t = useTranslations("Search");

// The error message is passed as an object that's been stringified.
// Parse it here.

let parsedErrorData;
let convertedSearchParams;

if (!isValidJSON(error.message)) {
// the error likely is just a string with a non-specific Server Component error when running the built app
// "An error occurred in the Server Components render. The specific message is omitted in production builds..."
parsedErrorData = getParsedError();
convertedSearchParams = parsedErrorData.searchInputs;
parsedErrorData = createBlankParsedError();
} else {
// Valid error thrown from server component
parsedErrorData = JSON.parse(error.message) as ParsedError;

// The error message search inputs had to be converted to arrays in order to be stringified,
// convert those back to sets as we do in non-error flow.
convertedSearchParams = convertSearchInputArraysToSets(
parsedErrorData.searchInputs,
);
}
const {
agency,
category,
eligibility,
fundingInstrument,
query,
sortby,
status,
} = convertedSearchParams;
const convertedSearchParams = convertSearchParamsToProperTypes(
parsedErrorData.searchInputs,
);
const { agency, category, eligibility, fundingInstrument, query, status } =
convertedSearchParams;

useEffect(() => {
console.error(error);
}, [error]);

return (
<>
<PageSEO
title="Search Funding Opportunities"
description="Try out our experimental search page."
/>
<BetaAlert />
<Breadcrumbs breadcrumbList={SEARCH_CRUMBS} />
<SearchCallToAction />
<QueryProvider>
<div className="grid-container">
<div className="search-bar">
<SearchBar query={query} />
</div>
<div className="grid-row grid-gap">
<div className="tablet:grid-col-4">
<SearchOpportunityStatus query={status} />
<SearchFilterAccordion
filterOptions={fundingOptions}
title="Funding instrument"
queryParamKey="fundingInstrument"
query={fundingInstrument}
/>
<SearchFilterAccordion
filterOptions={eligibilityOptions}
title="Eligibility"
queryParamKey="eligibility"
query={eligibility}
/>
<SearchFilterAccordion
filterOptions={agencyOptions}
title="Agency"
queryParamKey="agency"
query={agency}
/>
<SearchFilterAccordion
filterOptions={categoryOptions}
title="Category"
queryParamKey="category"
query={category}
<QueryProvider>
<div className="grid-container">
<div className="search-bar">
<SearchBar query={query} />
</div>
<div className="grid-row grid-gap">
<div className="tablet:grid-col-4">
<ContentDisplayToggle
showCallToAction={t("filterDisplayToggle.showFilters")}
hideCallToAction={t("filterDisplayToggle.hideFilters")}
breakpoint={Breakpoints.TABLET}
>
<SearchFilters
opportunityStatus={status}
eligibility={eligibility}
category={category}
fundingInstrument={fundingInstrument}
agency={agency}
/>
</div>
<div className="tablet:grid-col-8">
<SearchResultsHeader sortby={sortby} />
<div className="usa-prose">
<SearchErrorAlert />
</div>
</div>
</ContentDisplayToggle>
</div>
<div className="tablet:grid-col-8">
<SearchErrorAlert />
</div>
</div>
</QueryProvider>
</>
</div>
</QueryProvider>
);
}

function convertSearchInputArraysToSets(
searchInputs: QueryParamData,
): QueryParamData {
return {
...searchInputs,
status: new Set(searchInputs.status || []),
fundingInstrument: new Set(searchInputs.fundingInstrument || []),
eligibility: new Set(searchInputs.eligibility || []),
agency: new Set(searchInputs.agency || []),
category: new Set(searchInputs.category || []),
};
}

function isValidJSON(str: string) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false; // String is not valid JSON
}
}

function getParsedError() {
return {
type: "NetworkError",
searchInputs: {
status: new Set(),
fundingInstrument: new Set(),
eligibility: new Set(),
agency: new Set(),
category: new Set(),
sortby: null,
page: 1,
actionType: "initialLoad",
},
message: "Invalid JSON returned",
status: -1,
} as ParsedError;
}
27 changes: 27 additions & 0 deletions frontend/src/app/[locale]/search/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SEARCH_CRUMBS } from "src/constants/breadcrumbs";

import { useTranslations } from "next-intl";
import { unstable_setRequestLocale } from "next-intl/server";

import BetaAlert from "src/components/BetaAlert";
import Breadcrumbs from "src/components/Breadcrumbs";
import PageSEO from "src/components/PageSEO";
import SearchCallToAction from "src/components/search/SearchCallToAction";

export default function SearchLayout({
children,
}: {
children: React.ReactNode;
}) {
unstable_setRequestLocale("en");
const t = useTranslations("Search");
return (
<>
<PageSEO title={t("title")} description={t("meta_description")} />
<BetaAlert />
<Breadcrumbs breadcrumbList={SEARCH_CRUMBS} />
<SearchCallToAction />
{children}
</>
);
}
91 changes: 9 additions & 82 deletions frontend/src/app/[locale]/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
import { Metadata } from "next";
import Loading from "src/app/[locale]/search/loading";
import QueryProvider from "src/app/[locale]/search/QueryProvider";
import { SEARCH_CRUMBS } from "src/constants/breadcrumbs";
import withFeatureFlag from "src/hoc/search/withFeatureFlag";
import { Breakpoints } from "src/types/uiTypes";
import { convertSearchParamsToProperTypes } from "src/utils/search/convertSearchParamsToProperTypes";

import { useTranslations } from "next-intl";
import { getTranslations, unstable_setRequestLocale } from "next-intl/server";
import { Suspense } from "react";

import BetaAlert from "src/components/BetaAlert";
import Breadcrumbs from "src/components/Breadcrumbs";
import ContentDisplayToggle from "src/components/ContentDisplayToggle";
import PageSEO from "src/components/PageSEO";
import SearchBar from "src/components/search/SearchBar";
import SearchCallToAction from "src/components/search/SearchCallToAction";
import SearchFilters from "src/components/search/SearchFilters";
import SearchPagination from "src/components/search/SearchPagination";
import SearchPaginationFetch from "src/components/search/SearchPaginationFetch";
import SearchResultsHeader from "src/components/search/SearchResultsHeader";
import SearchResultsHeaderFetch from "src/components/search/SearchResultsHeaderFetch";
import SearchResultsListFetch from "src/components/search/SearchResultsListFetch";
import SearchResults from "src/components/search/SearchResults";
import QueryProvider from "./QueryProvider";

export async function generateMetadata() {
const t = await getTranslations({ locale: "en" });
Expand All @@ -48,30 +37,15 @@ function Search({ searchParams }: { searchParams: searchParamsTypes }) {
unstable_setRequestLocale("en");
const t = useTranslations("Search");
const convertedSearchParams = convertSearchParamsToProperTypes(searchParams);
const {
agency,
category,
eligibility,
fundingInstrument,
page,
query,
sortby,
status,
} = convertedSearchParams;
const { agency, category, eligibility, fundingInstrument, query, status } =
convertedSearchParams;

if (!("page" in searchParams)) {
searchParams.page = "1";
}
const key = Object.entries(searchParams).join(",");
const pager1key = Object.entries(searchParams).join("-") + "pager1";
const pager2key = Object.entries(searchParams).join("-") + "pager2";

return (
<>
<PageSEO title={t("title")} description={t("meta_description")} />
<BetaAlert />
<Breadcrumbs breadcrumbList={SEARCH_CRUMBS} />
<SearchCallToAction />
<QueryProvider>
<div className="grid-container">
<div className="search-bar">
Expand All @@ -94,58 +68,11 @@ function Search({ searchParams }: { searchParams: searchParamsTypes }) {
</ContentDisplayToggle>
</div>
<div className="tablet:grid-col-8">
<Suspense
key={key}
fallback={
<SearchResultsHeader sortby={sortby} loading={false} />
}
>
<SearchResultsHeaderFetch
sortby={sortby}
queryTerm={query}
searchParams={convertedSearchParams}
/>
</Suspense>
<div className="usa-prose">
<Suspense
key={pager1key}
fallback={
<SearchPagination
loading={true}
page={page}
query={query}
/>
}
>
<SearchPaginationFetch
searchParams={convertedSearchParams}
scroll={false}
/>
</Suspense>
<Suspense
key={key}
fallback={<Loading message={t("loading")} />}
>
<SearchResultsListFetch
searchParams={convertedSearchParams}
/>
</Suspense>
<Suspense
key={pager2key}
fallback={
<SearchPagination
loading={true}
page={page}
query={query}
/>
}
>
<SearchPaginationFetch
searchParams={convertedSearchParams}
scroll={true}
/>
</Suspense>
</div>
<SearchResults
searchParams={convertedSearchParams}
query={query}
loadingMessage={t("loading")}
></SearchResults>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit ffaaa27

Please sign in to comment.