Repository: rewiringamerica/incentives-visualizer
Pull request #75: Added search by county
Author: login: etong11
Labels: name: Must Have
name: Easy
name: P1
Created: 2025-04-10T02:11:39Z | Merged: 2025-04-15T02:13:30Z

<!-- Provide a general summary of your changes in the Title above -->

## Description

<!-- Describe your changes in detail -->
- Added counties to the list of search options
- Searching a county selects and zooms into that county

## Related Issue

<!-- This project only accepts pull requests related to open issues -->
<!-- If suggesting a new feature or change, please discuss it in an issue first -->
<!-- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!-- Please link to the issue here: -->
Closes #74 

## Motivation and Context

<!-- Why is this change required? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here. -->
Allowing users to select a county with the search bar to view incentive information and to navigate to the county is an easier form of navigation for the user. 

## How Has This Been Tested?

<!-- Please describe in detail how you tested your changes. -->
<!-- Include details of your testing environment, and the tests you ran to -->
<!-- see how your change affects other areas of the code, etc. -->
<!-- If no tests were run, reply with N/A -->
Visual testing was used.
- Enter county name in the search bar and submit search. Should zoom to county and select the county in the sidebar.
- Existing search by state functionality should be maintained.

## Screenshots (if appropriate):
Note that the sidebar is not set up to display county data yet
<img width="1710" alt="county search options for California" src="https://github.com/user-attachments/assets/caa89501-672a-4a66-a1fc-848433a55117" />
<img width="1710" alt="image" src="https://github.com/user-attachments/assets/79a037c6-c6b3-48ad-a6fc-1d2c88d351ad" />


Linked issue #74: Add search by county
Prerequisites
- Search bar must be created.
- Map and state layers with state names as data must be set up.

Tasks
- Provide autosuggestions in search bar based on available county names
- Implement searching by county in search bar
  - Only allow valid county names to be submitted
  - On submission of search, should select county in sidebar and zoom into county

Diff:
diff --git a/src/components/App.tsx b/src/components/App.tsx
index ad8636b..5a3467b 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -1,5 +1,6 @@
 import React, { useCallback, useState } from 'react';
 import ReactDOM from 'react-dom/client';
+import { zoomToCounty } from './Counties';
 import Map from './Map';
 import NavBar from './Navbar';
 import Sidebar from './Sidebar';
@@ -42,7 +43,9 @@ const App: React.FC = () => {
       <NavBar
         map={mapInstance}
         zoomToState={zoomToState}
-        onFeatureSelect={handleStateSelect}
+        zoomToCounty={zoomToCounty}
+        onStateSelect={handleStateSelect}
+        onCountySelect={handleCountySelect}
       />
       <div className="flex h-[calc(100vh-64px)]">
         <Sidebar
diff --git a/src/components/Counties.tsx b/src/components/Counties.tsx
index c3cd0c2..31c6ffa 100644
--- a/src/components/Counties.tsx
+++ b/src/components/Counties.tsx
@@ -172,4 +172,4 @@ function zoomToCounty(
   });
 }
 
-export { loadCounties };
+export { loadCounties, zoomToCounty };
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index c8457e2..7ec9dae 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,8 +1,8 @@
 import { Autocomplete, TextField } from '@mui/material';
 import React, { useState } from 'react';
 import logo from '../assets/logo.png';
-import geojsonData from '../data/geojson/states-albers.json';
-import { US_STATE_NAMES } from '../data/states';
+import countyGeojson from '../data/geojson/counties-albers.json';
+import stateGeojson from '../data/geojson/states-albers.json';
 
 interface NavbarProps {
   map: maplibregl.Map;
@@ -10,16 +10,73 @@ interface NavbarProps {
     map: maplibregl.Map,
     feature: maplibregl.MapGeoJSONFeature,
   ) => void;
-  onFeatureSelect: (feature: maplibregl.MapGeoJSONFeature) => void;
+  zoomToCounty: (
+    map: maplibregl.Map,
+    feature: maplibregl.MapGeoJSONFeature,
+  ) => void;
+  onStateSelect: (feature: maplibregl.MapGeoJSONFeature) => void;
+  onCountySelect: (feature: maplibregl.MapGeoJSONFeature) => void;
 }
 
+type SearchOption = {
+  label: string;
+  type: 'state' | 'county';
+  feature?: maplibregl.MapGeoJSONFeature;
+};
+
 const Navbar: React.FC<NavbarProps> = ({
   map,
   zoomToState,
-  onFeatureSelect,
+  zoomToCounty,
+  onStateSelect,
+  onCountySelect,
 }) => {
   const [searchTerm, setSearchTerm] = useState('');
 
+  const countiesFiltered = countyGeojson.features.filter(feature => {
+    const coty_type = feature.properties?.coty_type;
+    return coty_type && coty_type === 'county';
+  });
+
+  const statesFiltered = stateGeojson.features.filter(feature => {
+    const ste_type = feature.properties?.ste_type;
+    return ste_type && ste_type === 'state';
+  });
+
+  const searchOptions: SearchOption[] = [
+    ...statesFiltered.map(feature => {
+      const stateName = Array.isArray(feature.properties?.ste_name)
+        ? feature.properties.ste_name[0]
+        : feature.properties?.ste_name;
+
+      let stateDisplayName = stateName;
+      if (stateName === 'District of Columbia') {
+        stateDisplayName = 'Washington, D.C.';
+      }
+
+      return {
+        label: stateDisplayName,
+        type: 'state',
+        feature,
+      };
+    }),
+    ...countiesFiltered.map(feature => {
+      const countyName = Array.isArray(feature.properties?.coty_name)
+        ? feature.properties.coty_name[0]
+        : feature.properties?.coty_name;
+
+      const stateName = Array.isArray(feature.properties?.ste_name)
+        ? feature.properties.ste_name[0]
+        : feature.properties?.ste_name;
+
+      return {
+        label: `${countyName}, ${stateName}`,
+        type: 'county',
+        feature,
+      };
+    }),
+  ];
+
   const handleSearchChange = (
     event: React.SyntheticEvent<Element, Event>,
     value: string,
@@ -29,23 +86,26 @@ const Navbar: React.FC<NavbarProps> = ({
 
   const handleSubmit = () => {
     if (searchTerm.trim()) {
-      const stateName = searchTerm.trim();
+      const searchValue = searchTerm.trim();
 
-      const feature = geojsonData.features.find(f => {
-        const steNameRaw = f.properties?.ste_name;
-        // Since steName is in form ['name'], we need to parse it
-        const steName = Array.isArray(steNameRaw)
-          ? steNameRaw[0]
-          : steNameRaw?.replace(/^\['|'\]$/g, '');
+      const searchOption = searchOptions.find(
+        option => option.label === searchValue,
+      );
+      if (!searchOption) {
+        console.error(`State/county "${searchValue}" not found.`);
+        return;
+      }
 
-        return steName === stateName;
-      });
+      const feature = searchOption.feature;
 
-      if (feature) {
-        onFeatureSelect(feature as maplibregl.MapGeoJSONFeature);
-        zoomToState(map, feature as maplibregl.MapGeoJSONFeature);
+      if (feature && searchOption.type === 'state') {
+        onStateSelect(feature);
+        zoomToState(map, feature);
+      } else if (feature && searchOption.type === 'county') {
+        onCountySelect(feature);
+        zoomToCounty(map, feature);
       } else {
-        console.error(`State "${searchTerm}" not found.`);
+        console.error(`State/county "${searchTerm}" not found.`);
       }
     }
   };
@@ -68,7 +128,7 @@ const Navbar: React.FC<NavbarProps> = ({
         style={{ maxWidth: '400px', width: '100%' }}
       >
         <Autocomplete
-          options={US_STATE_NAMES}
+          options={searchOptions}
           inputValue={searchTerm}
           onInputChange={(event, value) => handleSearchChange(event, value)}
           disableClearable


Reviews:
user: login: DanWilkerson
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-13T05:08:35Z
user: login: etong11
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T18:35:01Z
user: login: mgmgperson
body: New changes look good! Opened up new issue for zoom bug fixing.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T02:13:22Z

Review comments:
diff_hunk: @@ -26,28 +97,27 @@ const Navbar: React.FC<NavbarProps> = ({ map, zoomToState, onStateSelect }) => {
 
   const handleSubmit = () => {
     if (searchTerm.trim()) {
-      const stateName = searchTerm.trim();
-
-      const feature = geojsonData.features.find(f => {
-        const steNameRaw = f.properties?.ste_name;
-        // Since steName is in form ['name'], we need to parse it
-        const steName = Array.isArray(steNameRaw)
-          ? steNameRaw[0]
-          : steNameRaw?.replace(/^\['|'\]$/g, '');
+      const searchValue = searchTerm.trim();
 
-        return steName === stateName;
-      });
+      const searchOption = searchOptions.find(
+        option => option.label === searchValue,
path: src/components/Navbar.tsx
user: login: DanWilkerson
body: What happens if a county and state share a name, or when there are multiple matches?
created_at: 2025-04-13T05:08:35Z
author_association: NONE
diff_hunk: @@ -26,28 +97,27 @@ const Navbar: React.FC<NavbarProps> = ({ map, zoomToState, onStateSelect }) => {
 
   const handleSubmit = () => {
     if (searchTerm.trim()) {
-      const stateName = searchTerm.trim();
-
-      const feature = geojsonData.features.find(f => {
-        const steNameRaw = f.properties?.ste_name;
-        // Since steName is in form ['name'], we need to parse it
-        const steName = Array.isArray(steNameRaw)
-          ? steNameRaw[0]
-          : steNameRaw?.replace(/^\['|'\]$/g, '');
+      const searchValue = searchTerm.trim();
 
-        return steName === stateName;
-      });
+      const searchOption = searchOptions.find(
+        option => option.label === searchValue,
path: src/components/Navbar.tsx
user: login: etong11
body: The label is either the state name if the feature is a state or "county name, state name" if the feature is a county. This is unique for each county (a state doesn't have more than one county with the same name). Furthermore, the search only allows submission of values that match an existing label in the search options so that a user can't submit a non-valid value. This ensures that there will only be one match (or no match at all).
created_at: 2025-04-13T18:35:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-10T02:11:44Z
body: [vc]: #42NHy8XiTQt/VMpLuyzZxM3cMQMUJhABwX2tI6cMclg=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJpbmNlbnRpdmVzLXZpc3VhbGl6ZXIiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiaW5jZW50aXZlcy12aXN1YWxpemVyLWdpdC1mZWF0LWNvdW50eS1zZWFyY2gtcmV3aXJpbmctYW1lcmljYS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9yZXdpcmluZy1hbWVyaWNhL2luY2VudGl2ZXMtdmlzdWFsaXplci9BVzU4RHhNcGVyU1djRlp0enV3TVFpcmI0RGhuIiwicHJldmlld1VybCI6ImluY2VudGl2ZXMtdmlzdWFsaXplci1naXQtZmVhdC1jb3VudHktc2VhcmNoLXJld2lyaW5nLWFtZXJpY2EudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **incentives-visualizer** | ✅ Ready ([Inspect](https://vercel.com/rewiring-america/incentives-visualizer/AW58DxMperSWcFZtzuwMQirb4Dhn)) | [Visit Preview](https://incentives-visualizer-git-feat-county-search-rewiring-america.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/incentives-visualizer-git-feat-county-search-rewiring-america.vercel.app?via=pr-comment-feedback-link) | Apr 15, 2025 1:56am |


author_association: NONE
user: login: DanWilkerson
created_at: 2025-04-14T03:25:11Z
body: Perfect, thanks.

On Mon, Apr 14, 2025 at 03:35 Emma Tong ***@***.***> wrote:

> ***@***.**** commented on this pull request.
> ------------------------------
>
> In src/components/Navbar.tsx
> <https://github.com/rewiringamerica/incentives-visualizer/pull/75#discussion_r2041183145>
> :
>
> >
> -        return steName === stateName;
> -      });
> +      const searchOption = searchOptions.find(
> +        option => option.label === searchValue,
>
> The label is either the state name if the feature is a state or "county
> name, state name" if the feature is a county. This is unique for each
> county (a state doesn't have more than one county with the same name).
> Furthermore, the search only allows submission of values that match an
> existing label in the search options so that a user can't submit a
> non-valid value. This ensures that there will only be one match (or no
> match at all).
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/rewiringamerica/incentives-visualizer/pull/75#discussion_r2041183145>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABMM2DIWRJ2AUNNST6GKIE32ZKU6VAVCNFSM6AAAAAB22MV2AGVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDONRSHAYTMOBVGM>
> .
> You are receiving this because you commented.Message ID:
> ***@***.***
> com>
>

author_association: NONE
user: login: eslothower
created_at: 2025-04-15T00:19:42Z
body: Please remerge main into this branch, and fix conflicts. Thank you!
author_association: CONTRIBUTOR