This repository has been archived by the owner on Sep 18, 2024. It is now read-only.
forked from HHS/simpler-grants-gov
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Fixes HHS#2068 - Add new id-based opportunity page - Add new `OpportunityListingAPI` class extended from `BaseAPI` - Make `searchInputs`/`QueryParamData` in `BaseAPI` and `errors.ts` optional params (only used for search page) - Update sitemap to replace [id] in url with 1 - Add test coverage
- Loading branch information
Showing
11 changed files
with
393 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { | ||
ApiResponse, | ||
Summary, | ||
} from "../../../../types/opportunity/opportunityResponseTypes"; | ||
|
||
import { Metadata } from "next"; | ||
import NotFound from "../../../not-found"; | ||
import OpportunityListingAPI from "../../../api/OpportunityListingAPI"; | ||
import { getTranslations } from "next-intl/server"; | ||
import { isSummary } from "../../../../utils/opportunity/isSummary"; | ||
|
||
export async function generateMetadata() { | ||
const t = await getTranslations({ locale: "en" }); | ||
const meta: Metadata = { | ||
title: t("OpportunityListing.page_title"), | ||
description: t("OpportunityListing.meta_description"), | ||
}; | ||
return meta; | ||
} | ||
|
||
export default async function OpportunityListing({ | ||
params, | ||
}: { | ||
params: { id: string }; | ||
}) { | ||
const id = Number(params.id); | ||
|
||
// Opportunity id needs to be a number greater than 1 | ||
if (isNaN(id) || id < 0) { | ||
return <NotFound />; | ||
} | ||
|
||
const api = new OpportunityListingAPI(); | ||
let opportunity: ApiResponse; | ||
try { | ||
opportunity = await api.getOpportunityById(id); | ||
} catch (error) { | ||
console.error("Failed to fetch opportunity:", error); | ||
return <NotFound />; | ||
} | ||
|
||
if (!opportunity.data) { | ||
return <NotFound />; | ||
} | ||
|
||
const renderSummary = (summary: Summary) => { | ||
return ( | ||
<> | ||
{Object.entries(summary).map(([summaryKey, summaryValue]) => ( | ||
<tr key={summaryKey}> | ||
<td className="word-wrap">{`summary.${summaryKey}`}</td> | ||
<td className="word-wrap">{JSON.stringify(summaryValue)}</td> | ||
</tr> | ||
))} | ||
</> | ||
); | ||
}; | ||
|
||
return ( | ||
<div className="grid-container"> | ||
<div className="grid-row margin-y-4"> | ||
<div className="usa-table-container"> | ||
<table className="usa-table usa-table--borderless margin-x-auto width-full maxw-desktop-lg"> | ||
<thead> | ||
<tr> | ||
<th>Field Name</th> | ||
<th>Data</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{Object.entries(opportunity.data).map(([key, value]) => { | ||
if (key === "summary" && isSummary(value)) { | ||
return renderSummary(value); | ||
} else { | ||
return ( | ||
<tr key={key}> | ||
<td className="word-wrap">{key}</td> | ||
<td className="word-wrap">{JSON.stringify(value)}</td> | ||
</tr> | ||
); | ||
} | ||
})} | ||
</tbody> | ||
</table> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import "server-only"; | ||
|
||
import { ApiResponse } from "../../types/opportunity/opportunityResponseTypes"; | ||
import BaseApi from "./BaseApi"; | ||
|
||
export default class OpportunityListingAPI extends BaseApi { | ||
get version(): string { | ||
return "v1"; | ||
} | ||
|
||
get basePath(): string { | ||
return process.env.API_URL || ""; | ||
} | ||
|
||
get namespace(): string { | ||
return "opportunities"; | ||
} | ||
|
||
async getOpportunityById(opportunityId: number): Promise<ApiResponse> { | ||
const subPath = `${opportunityId}`; | ||
const response = await this.request( | ||
"GET", | ||
this.basePath, | ||
this.namespace, | ||
subPath, | ||
); | ||
return response as ApiResponse; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
frontend/src/types/opportunity/opportunityResponseTypes.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
export interface OpportunityAssistanceListing { | ||
assistance_listing_number: string; | ||
program_title: string; | ||
} | ||
|
||
export interface Summary { | ||
additional_info_url: string; | ||
additional_info_url_description: string; | ||
agency_code: string; | ||
agency_contact_description: string; | ||
agency_email_address: string; | ||
agency_email_address_description: string; | ||
agency_name: string; | ||
agency_phone_number: string; | ||
applicant_eligibility_description: string; | ||
applicant_types: string[]; | ||
archive_date: string; | ||
award_ceiling: number; | ||
award_floor: number; | ||
close_date: string; | ||
close_date_description: string; | ||
estimated_total_program_funding: number; | ||
expected_number_of_awards: number; | ||
fiscal_year: number; | ||
forecasted_award_date: string; | ||
forecasted_close_date: string; | ||
forecasted_close_date_description: string; | ||
forecasted_post_date: string; | ||
forecasted_project_start_date: string; | ||
funding_categories: string[]; | ||
funding_category_description: string; | ||
funding_instruments: string[]; | ||
is_cost_sharing: boolean; | ||
is_forecast: boolean; | ||
post_date: string; | ||
summary_description: string; | ||
} | ||
|
||
export interface Opportunity { | ||
agency: string; | ||
category: string; | ||
category_explanation: string | null; | ||
created_at: string; | ||
opportunity_assistance_listings: OpportunityAssistanceListing[]; | ||
opportunity_id: number; | ||
opportunity_number: string; | ||
opportunity_status: string; | ||
opportunity_title: string; | ||
summary: Summary; | ||
updated_at: string; | ||
} | ||
|
||
export interface ApiResponse { | ||
data: Opportunity[]; | ||
message: string; | ||
status_code: number; | ||
} |
Oops, something went wrong.