Skip to content

Commit

Permalink
[Issue #1382]: Move search page to app router (#1358)
Browse files Browse the repository at this point in the history
## Summary
Fixes #1382 

### Time to review: 5 min

## Changes proposed
- Move search page over to app router
  • Loading branch information
rylew1 authored Mar 1, 2024
1 parent e22fba3 commit ad2cb64
Show file tree
Hide file tree
Showing 15 changed files with 398 additions and 152 deletions.
1 change: 1 addition & 0 deletions frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
258 changes: 203 additions & 55 deletions frontend/package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
},
"dependencies": {
"@next/third-parties": "^14.0.4",
"@trussworks/react-uswds": "^5.0.0",
"@trussworks/react-uswds": "^7.0.0",
"@uswds/uswds": "^3.6.0",
"i18next": "^23.0.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"next": "^13.5.2",
"next": "^14.0.3",
"next-i18next": "^15.0.0",
"next-intl": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.0",
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import "src/styles/styles.scss";

import Layout from "src/components/AppLayout";
/**
* Root layout component, wraps all pages.
* @see https://nextjs.org/docs/app/api-reference/file-conventions/layout
*/
import { Metadata } from "next";

export const metadata: Metadata = {
icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/favicon.ico`],
};

interface LayoutProps {
children: React.ReactNode;

// TODO: use for i18n when ready
// params: {
// locale: string;
// };
}

export default function RootLayout({ children }: LayoutProps) {
return (
<html lang="en">
<body>
{/* Separate layout component for the inner-body UI elements since Storybook
and tests trip over the fact that this file renders an <html> tag */}

{/* TODO: Add locale="english" prop when ready for i18n */}
<Layout>{children}</Layout>
</body>
</html>
);
}
60 changes: 60 additions & 0 deletions frontend/src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import React, { useState } from "react";
import {
SearchFetcher,
fetchSearchOpportunities,
} from "../../services/searchfetcher/SearchFetcher";

import { APISearchFetcher } from "../../services/searchfetcher/APISearchFetcher";
import { MockSearchFetcher } from "../../services/searchfetcher/MockSearchFetcher";
import { Opportunity } from "../../types/searchTypes";
import PageNotFound from "../../pages/404";
import { useFeatureFlags } from "src/hooks/useFeatureFlags";

const useMockData = true;
const searchFetcher: SearchFetcher = useMockData
? new MockSearchFetcher()
: new APISearchFetcher();

// TODO: use for i18n when ready
// interface RouteParams {
// locale: string;
// }

// interface SearchProps {
// initialOpportunities: Opportunity[];
// }

export default function Search() {
const { featureFlagsManager, mounted } = useFeatureFlags();
const [searchResults, setSearchResults] = useState<Opportunity[]>([]);

const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
performSearch().catch((e) => console.log(e));
};

const performSearch = async () => {
const opportunities = await fetchSearchOpportunities(searchFetcher);
setSearchResults(opportunities);
};

if (!mounted) return null;
if (!featureFlagsManager.isFeatureEnabled("showSearchV0")) {
return <PageNotFound />;
}

return (
<>
<button onClick={handleButtonClick}>Update Results</button>
<ul>
{searchResults.map((opportunity) => (
<li key={opportunity.id}>
{opportunity.id}, {opportunity.title}
</li>
))}
</ul>
</>
);
}
26 changes: 26 additions & 0 deletions frontend/src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Footer from "./Footer";
import GrantsIdentifier from "./GrantsIdentifier";
import Header from "./Header";

type Props = {
children: React.ReactNode;
// TODO: pass locale into Layout when we setup i18n
// locale?: string;
};

const Layout = ({ children }: Props) => {
return (
// Stick the footer to the bottom of the page
<div className="display-flex flex-column minh-viewport">
<a className="usa-skipnav" href="#main-content">
{"skip_to_main"}
</a>
<Header />
<main id="main-content">{children}</main>
<Footer />
<GrantsIdentifier />
</div>
);
};

export default Layout;
2 changes: 2 additions & 0 deletions frontend/src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { ExternalRoutes } from "src/constants/routes";
import { assetPath } from "src/utils/assetPath";

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/GrantsIdentifier.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { ExternalRoutes } from "src/constants/routes";

import { Trans, useTranslation } from "next-i18next";
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { assetPath } from "src/utils/assetPath";

import { useTranslation } from "next-i18next";
Expand Down
74 changes: 0 additions & 74 deletions frontend/src/pages/search.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion frontend/stories/pages/search.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meta } from "@storybook/react";
import Search from "src/pages/search";
import Search from "../../src/app/search/page";

const meta: Meta<typeof Search> = {
title: "Pages/Search",
Expand Down
29 changes: 29 additions & 0 deletions frontend/tests/components/AppLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render, screen } from "@testing-library/react";
import { axe } from "jest-axe";

import AppLayout from "src/components/AppLayout";

describe("AppLayout", () => {
it("renders children in main section", () => {
render(
<AppLayout>
<h1>child</h1>
</AppLayout>,
);

const header = screen.getByRole("heading", { name: /child/i, level: 1 });

expect(header).toBeInTheDocument();
});

it("passes accessibility scan", async () => {
const { container } = render(
<AppLayout>
<h1>child</h1>
</AppLayout>,
);
const results = await axe(container);

expect(results).toHaveNoViolations();
});
});
3 changes: 1 addition & 2 deletions frontend/tests/components/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ const props = {
describe("Header", () => {
it("toggles the mobile nav menu", async () => {
render(<Header {...props} />);

const menuButton = screen.getByRole("button", { name: /menu/i });
const menuButton = screen.getByTestId("navMenuButton");

expect(menuButton).toBeInTheDocument();
expect(screen.getByRole("link", { name: /home/i })).toHaveAttribute(
Expand Down
36 changes: 20 additions & 16 deletions frontend/tests/pages/search.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { axe } from "jest-axe";
import { useFeatureFlags } from "src/hooks/useFeatureFlags";
import Search from "src/pages/search";

import { MOCKOPPORTUNITIES } from "../../src/services/searchfetcher/MockSearchFetcher";
import Search from "../../src/app/search/page";

jest.mock("src/hooks/useFeatureFlags");

Expand All @@ -21,28 +20,33 @@ const setFeatureFlag = (flag: string, value: boolean) => {
describe("Search", () => {
it("passes accessibility scan", async () => {
setFeatureFlag("showSearchV0", true);
const { container } = render(
<Search initialOpportunities={MOCKOPPORTUNITIES} />,
);
const { container } = render(<Search />);
const results = await waitFor(() => axe(container));
expect(results).toHaveNoViolations();
});

describe("Search feature flag", () => {
it("renders search results when feature flag is on", () => {
it("renders search results when feature flag is on", async () => {
setFeatureFlag("showSearchV0", true);
render(<Search initialOpportunities={MOCKOPPORTUNITIES} />);
expect(screen.getByText(/sunt aut/i)).toBeInTheDocument();
render(<Search />);
fireEvent.click(screen.getByRole("button", { name: /update results/i }));

await waitFor(() => {
expect(screen.getByText(/sunt aut/i)).toBeInTheDocument();
});
});

it("renders PageNotFound when feature flag is off", () => {
it("renders PageNotFound when feature flag is off", async () => {
setFeatureFlag("showSearchV0", false);
render(<Search initialOpportunities={MOCKOPPORTUNITIES} />);
expect(
screen.getByText(
/The page you have requested cannot be displayed because it does not exist, has been moved/i,
),
).toBeInTheDocument();
render(<Search />);

await waitFor(() => {
expect(
screen.getByText(
/The page you have requested cannot be displayed because it does not exist, has been moved/i,
),
).toBeInTheDocument();
});
});
});
});
15 changes: 13 additions & 2 deletions frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,19 @@
"@next/third-parties/google": [
"./node_modules/@next/third-parties/dist/google"
]
}
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "__mocks__/styleMock.js"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"__mocks__/styleMock.js",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}

0 comments on commit ad2cb64

Please sign in to comment.