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=