diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 9569f16..4d76cbc 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -20,3 +20,4 @@ jobs: team: ${{secrets.HEROKU_TEAM}} env: HD_VITE_ZONING_API_URL: ${{secrets.VITE_ZONING_API_URL}} + HD_VITE_CPDB_DATA_URL: ${{secrets.VITE_CPDB_DATA_URL}} diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 95261bf..7683b5f 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -23,3 +23,4 @@ jobs: team: ${{secrets.HEROKU_TEAM}} env: HD_VITE_ZONING_API_URL: ${{secrets.VITE_ZONING_API_URL}} + HD_VITE_CPDB_DATA_URL: ${{secrets.VITE_CPDB_DATA_URL}} diff --git a/app/components/ExportDataModal.test.tsx b/app/components/ExportDataModal.test.tsx new file mode 100644 index 0000000..506f3e1 --- /dev/null +++ b/app/components/ExportDataModal.test.tsx @@ -0,0 +1,49 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { ExportDataModal } from "./ExportDataModal"; +import { act } from "react"; + +describe("Export Data Modal", () => { + const geography = "Community District MN05"; + const fileName = "community_district_manhattan_05.csv"; + + it("should have a button to open the modal", async () => { + render(); + + expect(screen.queryByText(/Data Export/)).not.toBeInTheDocument(); + await act(() => fireEvent.click(screen.getByText(/Export Data/))); + expect(screen.getByText(/Data Export/)).toBeInTheDocument(); + }); + + it("should show the current district", async () => { + render(); + + await act(() => fireEvent.click(screen.getByText(/Export Data/))); + expect(screen.getByText(geography)).toBeInTheDocument(); + }); + + it("should have a button to download the files", async () => { + render(); + + await act(() => fireEvent.click(screen.getByText(/Export Data/))); + expect( + screen.getByRole("link", { + name: "Export Data", + }), + ).toHaveAttribute("href", expect.stringContaining(fileName)); + }); + + it("should let user choose whether to download all districts", async () => { + render(); + + await act(() => fireEvent.click(screen.getByText(/Export Data/))); + await act(() => fireEvent.click(screen.getByText(/Include all districts/))); + expect( + screen.getByRole("link", { + name: "Export Data", + }), + ).toHaveAttribute( + "href", + expect.stringContaining("projects_in_geographies.zip"), + ); + }); +}); diff --git a/app/components/ExportDataModal.tsx b/app/components/ExportDataModal.tsx new file mode 100644 index 0000000..489fefb --- /dev/null +++ b/app/components/ExportDataModal.tsx @@ -0,0 +1,103 @@ +import { + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Button, + Text, + Switch, + Heading, + FormControl, + FormLabel, + Box, +} from "@nycplanning/streetscape"; + +import { useState } from "react"; +import { LinkBtn } from "./LinkBtn"; + +export interface ExportDataModalProps { + geography: string; + fileName: string; +} + +export function ExportDataModal({ geography, fileName }: ExportDataModalProps) { + const [allDistricts, setAllDistricts] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const onOpen = () => setIsOpen(true); + const onClose = () => setIsOpen(false); + + return ( + <> + + + + + + Data Export + + + + + + Geography + + {geography} + + + + + + Include all districts? + + + + setAllDistricts((allDistricts) => !allDistricts) + } + /> + + + + + + Export Data + + + + + + ); +} diff --git a/app/components/LinkBtn.test.tsx b/app/components/LinkBtn.test.tsx new file mode 100644 index 0000000..a65aa93 --- /dev/null +++ b/app/components/LinkBtn.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/react"; +import { LinkBtn } from "./LinkBtn"; + +describe("LinkBtn", () => { + it("should link to url while displaying text", () => { + const testUrl = "test.com"; + const testText = "Test Text"; + render({testText}); + screen.debug(); + expect( + screen.getByRole("link", { + name: "Test Text", + }), + ).toHaveAttribute("href", testUrl); + }); +}); diff --git a/app/components/LinkBtn.tsx b/app/components/LinkBtn.tsx new file mode 100644 index 0000000..613f019 --- /dev/null +++ b/app/components/LinkBtn.tsx @@ -0,0 +1,44 @@ +import { Link, LinkProps } from "@nycplanning/streetscape"; + +export type LinkBtnProps = LinkProps; +export function LinkBtn(props: LinkBtnProps) { + return ( + + {props.children} + + ); +} diff --git a/app/components/WelcomePanel/WelcomeContent.tsx b/app/components/WelcomePanel/WelcomeContent.tsx index 78415a3..5831595 100644 --- a/app/components/WelcomePanel/WelcomeContent.tsx +++ b/app/components/WelcomePanel/WelcomeContent.tsx @@ -37,10 +37,8 @@ export function WelcomeContent() { Select a project on the map to learn more about the relevant agencies and capital commitments, or filter by specific geographies to see all - projects in that area. - {/* TODO: add this line when export feature is added */} - {/* You can also export your selection as a CSV table - or ESRI Shapefile. */} + projects in that area. You can also export your selection as a CSV + table. diff --git a/app/routes/boroughs.$boroughId.community-districts.$communityDistrictId.capital-projects.tsx b/app/routes/boroughs.$boroughId.community-districts.$communityDistrictId.capital-projects.tsx index 2997735..2fc15a0 100644 --- a/app/routes/boroughs.$boroughId.community-districts.$communityDistrictId.capital-projects.tsx +++ b/app/routes/boroughs.$boroughId.community-districts.$communityDistrictId.capital-projects.tsx @@ -2,6 +2,7 @@ import { Flex } from "@nycplanning/streetscape"; import { json, LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import { CapitalProjectsPanel } from "~/components/CapitalProjectsList"; +import { ExportDataModal } from "~/components/ExportDataModal"; import { Pagination } from "~/components/Pagination"; import { findAgencies, @@ -50,13 +51,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { boroughsPromise, projectsByCommunityDistrictPromise, ]); - const boroughAbbr = boroughsResponse.boroughs.find( + const activeBorough = boroughsResponse.boroughs.find( (borough) => borough.id === boroughId, - )?.abbr; + ); return { capitalProjectsResponse, agencies: agenciesResponse.agencies, - boroughAbbr, + boroughAbbr: activeBorough?.abbr, + boroughTitle: activeBorough?.title, communityDistrictId, }; } @@ -66,6 +68,7 @@ export default function CapitalProjectsByBoroughIdCommunityDistrictId() { capitalProjectsResponse: { total: capitalProjectsTotal, capitalProjects }, agencies, boroughAbbr, + boroughTitle, communityDistrictId, } = useLoaderData(); @@ -82,6 +85,10 @@ export default function CapitalProjectsByBoroughIdCommunityDistrictId() { marginTop={"auto"} > + ); diff --git a/app/routes/city-council-districts.$cityCouncilDistrictId.capital-projects.tsx b/app/routes/city-council-districts.$cityCouncilDistrictId.capital-projects.tsx index 2cffe44..f7b9a32 100644 --- a/app/routes/city-council-districts.$cityCouncilDistrictId.capital-projects.tsx +++ b/app/routes/city-council-districts.$cityCouncilDistrictId.capital-projects.tsx @@ -4,6 +4,7 @@ import { useLoaderData } from "@remix-run/react"; import { CapitalProjectsPanel } from "../components/CapitalProjectsList"; import { Flex } from "@nycplanning/streetscape"; import { Pagination } from "~/components/Pagination"; +import { ExportDataModal } from "~/components/ExportDataModal"; export async function loader({ request, params }: LoaderFunctionArgs) { const url = new URL(request.url); @@ -62,6 +63,10 @@ export default function CapitalProjectsByCityCouncilDistrict() { marginTop={"auto"} > + ); diff --git a/package-lock.json b/package-lock.json index 35b916e..acecfa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", "@kubb/swagger-client": "^2.23.2", - "@nycplanning/streetscape": "^0.11.0", + "@nycplanning/streetscape": "^0.12.0", "@remix-run/node": "^2.7.2", "@remix-run/react": "^2.7.2", "@remix-run/serve": "^2.7.2", @@ -4758,9 +4758,9 @@ } }, "node_modules/@nycplanning/streetscape": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@nycplanning/streetscape/-/streetscape-0.11.0.tgz", - "integrity": "sha512-fAjSTkSB1ALPr+TDiB/XNe+klKIP5mDT01dH/Iyw+CZR3Y0hV2vfYR0SLaPrEPmCg0IsMj7oxiCD3EHPLaECdA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@nycplanning/streetscape/-/streetscape-0.12.0.tgz", + "integrity": "sha512-OuIxO2edUufjotYV7IRE5cmDZ/GN5eT5XIHvSX2IgDG7MDyNv7UmLERovfGSzin87DCnq3eTp+h3BGI08gvsvw==", "hasInstallScript": true, "dependencies": { "@chakra-ui/cli": "^2.4.1", diff --git a/package.json b/package.json index 70c7cac..035bcaf 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", "@kubb/swagger-client": "^2.23.2", - "@nycplanning/streetscape": "^0.11.0", + "@nycplanning/streetscape": "^0.12.0", "@remix-run/node": "^2.7.2", "@remix-run/react": "^2.7.2", "@remix-run/serve": "^2.7.2", diff --git a/sample.env b/sample.env index a00a49f..6b203c9 100644 --- a/sample.env +++ b/sample.env @@ -1 +1,2 @@ VITE_ZONING_API_URL=http://localhost:3000 +VITE_CPDB_DATA_URL=