Skip to content

Commit

Permalink
[Issue #2273] create opportunity call to action linking to legacy (#2543
Browse files Browse the repository at this point in the history
)

* Adds a CTA box on the opportunity detail page with link to legacy site
* upgrades the testing-library package to fix bug with querying for paragraph role
  • Loading branch information
doug-s-nava authored Oct 25, 2024
1 parent 9d22440 commit dfd3e74
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 43 deletions.
49 changes: 35 additions & 14 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
"@storybook/addon-essentials": "^7.1.0",
"@storybook/nextjs": "^7.1.0",
"@storybook/react": "^7.1.0",
"@testing-library/dom": "^9.0.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest-axe": "^3.5.5",
"@types/js-cookie": "^3.0.6",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/[locale]/opportunity/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GridContainer } from "@trussworks/react-uswds";
import BetaAlert from "src/components/BetaAlert";
import Breadcrumbs from "src/components/Breadcrumbs";
import OpportunityAwardInfo from "src/components/opportunity/OpportunityAwardInfo";
import OpportunityCTA from "src/components/opportunity/OpportunityCTA";
import OpportunityDescription from "src/components/opportunity/OpportunityDescription";
import OpportunityHistory from "src/components/opportunity/OpportunityHistory";
import OpportunityIntro from "src/components/opportunity/OpportunityIntro";
Expand Down Expand Up @@ -120,6 +121,7 @@ async function OpportunityListing({ params }: { params: { id: string } }) {

<div className="desktop:grid-col-4 tablet:grid-col-12 tablet:order-0">
<OpportunityStatusWidget opportunityData={opportunityData} />
<OpportunityCTA id={opportunityData.opportunity_id} />
<OpportunityAwardInfo opportunityData={opportunityData} />
<OpportunityHistory summary={opportunityData.summary} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const OpportunityAwardGridRow = ({ title, content }: Props) => {
const t = useTranslations("OpportunityListing.award_info");

return (
<div className="border radius-md border-base-lighter padding-x-2 ">
<div className="border radius-md border-base-lighter padding-x-2">
<p className="font-sans-sm text-bold margin-bottom-0">
{content || defaultContentByType(title)}
</p>
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/components/opportunity/OpportunityCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { environment } from "src/constants/environments";

import { useTranslations } from "next-intl";
import { ReactNode } from "react";
import { Button } from "@trussworks/react-uswds";

import { USWDSIcon } from "src/components/USWDSIcon";

export const OpportunityContentBox = ({
title,
content,
}: {
title?: string | ReactNode;
content: string | ReactNode;
}) => {
return (
<div className="border radius-md border-base-lighter padding-x-2">
{title && <p className="text-bold margin-bottom-0">{title}</p>}
<p className="desktop-lg:font-sans-sm margin-top-0">{content}</p>
</div>
);
};

const OpportunityCTA = ({ id }: { id: number }) => {
const t = useTranslations("OpportunityListing.cta");
const legacyOpportunityURL = `${environment.LEGACY_HOST}/search-results-detail/${id}`;

const content = (
<>
<span>{t("apply_content")}</span>
<a href={legacyOpportunityURL} target="_blank" rel="noopener noreferrer">
<Button type="button" outline={true} className="margin-top-2">
<span>{t("button_content")}</span>
<USWDSIcon
name="launch"
className="usa-icon usa-icon--size-4 text-middle"
/>
</Button>
</a>
</>
);

return (
<div className="usa-prose margin-top-2">
<OpportunityContentBox title={t("apply_title")} content={content} />
</div>
);
};

export default OpportunityCTA;
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const OpportunityStatusWidget = ({ opportunityData }: Props) => {
return (
<div className="usa-tag bg-base-dark border-radius-2 width-100 radius-md margin-right-0 font-sans-sm text-center text-no-uppercase">
<p>
<strong>{t("forecasted")} </strong>
<strong>{t("forecasted")}</strong>
</p>
</div>
);
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/constants/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export const PUBLIC_ENV = PUBLIC_ENV_VARS_BY_ENV[CURRENT_ENV];

// home for all interpreted server side environment variables
export const environment: { [key: string]: string } = {
LEGACY_HOST:
NODE_ENV === "production"
? "https://grants.gov"
: "https://test.grants.gov",
NEXT_PUBLIC_BASE_PATH: NEXT_PUBLIC_BASE_PATH ?? "",
USE_SEARCH_MOCK_DATA,
SENDY_API_URL: SENDY_API_URL || "",
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/i18n/messages/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export const messages = {
closing_warn:
"Electronically submitted applications must be submitted no later than 5:00 p.m., ET, on the listed application due date.",
},
cta: {
apply_title: "Application process",
apply_content:
"This site is a work in progress. Go to www.grants.gov to apply, track application status, and subscribe to updates.",
button_content: "View on Grants.gov",
},
},
Index: {
page_title: "Simpler.Grants.gov",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export class MockSearchFetcher extends SearchFetcher {
async fetchOpportunities(): Promise<SearchAPIResponse> {
// simulate delay
await new Promise((resolve) => setTimeout(resolve, 750));
return mockData;
return mockData as SearchAPIResponse;
}
}
4 changes: 3 additions & 1 deletion frontend/src/types/opportunity/opportunityResponseTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type OpportunityStatus = "archived" | "closed" | "posted" | "forecasted";

export interface OpportunityAssistanceListing {
assistance_listing_number: string;
program_title: string;
Expand Down Expand Up @@ -47,7 +49,7 @@ export interface Opportunity {
opportunity_assistance_listings: OpportunityAssistanceListing[];
opportunity_id: number;
opportunity_number: string;
opportunity_status: string;
opportunity_status: OpportunityStatus;
opportunity_title: string;
summary: Summary;
updated_at: string;
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/types/search/searchResponseTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PaginationInfo } from "src/types/apiResponseTypes";

import { SearchFetcherActionType } from "./searchRequestTypes";
import { OpportunityStatus } from "src/types/opportunity/opportunityResponseTypes";
import { SearchFetcherActionType } from "src/types/search/searchRequestTypes";

export interface AssistanceListing {
assistance_listing_number: string;
Expand Down Expand Up @@ -49,7 +49,7 @@ export interface Opportunity {
opportunity_assistance_listings: AssistanceListing[];
opportunity_id: number;
opportunity_number: string;
opportunity_status: string;
opportunity_status: OpportunityStatus;
opportunity_title: string;
summary: Summary;
updated_at: string;
Expand Down
4 changes: 2 additions & 2 deletions frontend/tests/api/OpportunityListingApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("OpportunityListingAPI", () => {
});

it("should return opportunity data for a valid ID", async () => {
const mockResponse: OpportunityApiResponse = getValidMockResponse();
const mockResponse = getValidMockResponse();

mockedRequest.mockResolvedValue(mockResponse);

Expand All @@ -41,7 +41,7 @@ describe("OpportunityListingAPI", () => {
});
});

function getValidMockResponse() {
function getValidMockResponse(): OpportunityApiResponse {
return {
data: {
agency: "US-ABC",
Expand Down
62 changes: 62 additions & 0 deletions frontend/tests/components/opportunity/OpportunityCTA.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { render, screen } from "@testing-library/react";
import { useTranslationsMock } from "src/utils/testing/intlMocks";

import OpportunityCTA, {
OpportunityContentBox,
} from "src/components/opportunity/OpportunityCTA";

jest.mock("next-intl", () => ({
useTranslations: () => useTranslationsMock(),
}));

describe("OpportunityCTA", () => {
it("renders the expected content and title", () => {
render(<OpportunityCTA id={1} />);

expect(screen.getByText("apply_title")).toBeInTheDocument();
expect(screen.getByText("apply_content")).toBeInTheDocument();
});

it("renders a link that links out to the opportunity detail on grants.gov", () => {
render(<OpportunityCTA id={1} />);

const link = screen.getByRole("link");
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
"href",
"https://test.grants.gov/search-results-detail/1",
);
});
});

describe("OpportunityContentBox", () => {
it("displays title if one is provided", () => {
render(<OpportunityContentBox title="fun title" content="fun content" />);
expect(screen.getByText("fun title")).toBeInTheDocument();
});
it("does not displays title if one is not provided", () => {
render(<OpportunityContentBox content="fun content" />);
expect(screen.getAllByRole("paragraph")).toHaveLength(1);
});
it("displays content as string or React children", () => {
const { rerender } = render(
<OpportunityContentBox title="fun title" content="fun content" />,
);
expect(screen.getByText("fun content")).toBeInTheDocument();

rerender(
<OpportunityContentBox
title="fun title"
content={
<>
<span>Some Stuff</span>
<button>A button</button>
</>
}
/>,
);

expect(screen.getByText("Some Stuff")).toBeInTheDocument();
expect(screen.getByRole("button")).toBeInTheDocument();
});
});
Loading

0 comments on commit dfd3e74

Please sign in to comment.