From eb721ea16e17f113c20e6a43dc14f023906ba90b Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Mon, 25 Aug 2025 11:29:35 -0500 Subject: [PATCH 1/3] Files ported; yarn clean has Update result-box.tsx --- .../components/form-select/form-select.tsx | 2 +- src/app/helpers/data.tsx | 2 + src/app/helpers/window-settings.ts | 1 + src/app/models/query-schools.ts | 7 ++- .../separatemap/{map-api.js => map-api.tsx} | 39 ++++++------ .../{map-context.js => map-context.tsx} | 41 +++++++------ .../filters/{filters.js => filters.tsx} | 38 ++++++------ .../separatemap/search-box/inputs/inputs.scss | 5 +- .../inputs/{inputs.js => inputs.tsx} | 61 ++++++++++++------- .../{result-box.js => result-box.tsx} | 20 +++--- .../search-box/{results.js => results.tsx} | 21 ++++--- .../{search-box.js => search-box.tsx} | 25 +++++--- .../{separatemap.js => separatemap.tsx} | 4 +- 13 files changed, 157 insertions(+), 109 deletions(-) rename src/app/pages/separatemap/{map-api.js => map-api.tsx} (72%) rename src/app/pages/separatemap/{map-context.js => map-context.tsx} (56%) rename src/app/pages/separatemap/search-box/filters/{filters.js => filters.tsx} (59%) rename src/app/pages/separatemap/search-box/inputs/{inputs.js => inputs.tsx} (67%) rename src/app/pages/separatemap/search-box/result-box/{result-box.js => result-box.tsx} (81%) rename src/app/pages/separatemap/search-box/{results.js => results.tsx} (56%) rename src/app/pages/separatemap/search-box/{search-box.js => search-box.tsx} (80%) rename src/app/pages/separatemap/{separatemap.js => separatemap.tsx} (94%) diff --git a/src/app/components/form-select/form-select.tsx b/src/app/components/form-select/form-select.tsx index 4d2c985dc..e505718b8 100644 --- a/src/app/components/form-select/form-select.tsx +++ b/src/app/components/form-select/form-select.tsx @@ -11,7 +11,7 @@ export default function FormSelect({ }: { label: string; name: string; - selectAttributes: object; + selectAttributes?: object; options: SelectItem[]; onValueUpdate?: (v: string) => void; }) { diff --git a/src/app/helpers/data.tsx b/src/app/helpers/data.tsx index 45a98d38e..b9574d1d0 100644 --- a/src/app/helpers/data.tsx +++ b/src/app/helpers/data.tsx @@ -32,6 +32,8 @@ export function useRefreshable(getter: () => T) { return React.useReducer(getter, getter()); } +export type SetHandle = ReturnType + // Each time the Set is updated, the handle is refreshed // That way, the Set doesn't have to be rebuilt export function useSet(initialValue:T[]=[]) { diff --git a/src/app/helpers/window-settings.ts b/src/app/helpers/window-settings.ts index ec3d5edab..f63f1e0e5 100644 --- a/src/app/helpers/window-settings.ts +++ b/src/app/helpers/window-settings.ts @@ -4,6 +4,7 @@ export type WindowWithSettings = typeof window & { accountHref: string; gatedContentEndpoint: string; renewalEndpoint: string; + mapboxPK: string; }; }; diff --git a/src/app/models/query-schools.ts b/src/app/models/query-schools.ts index 6eee68364..55918df36 100644 --- a/src/app/models/query-schools.ts +++ b/src/app/models/query-schools.ts @@ -13,6 +13,7 @@ export type SchoolInfo = { testimonial?: string; testimonial_name?: string; testimonial_position?: string; + current_year_savings: number; all_time_savings: number; location: string; total_school_enrollment: string | null; @@ -29,13 +30,13 @@ type Item = { fields: SchoolInfo; }; -type AugmentedInfo = { +export type AugmentedInfo = { pk: string; cityState: string; institutionalPartner: string; institutionType: string; fields: SchoolInfo; - lngLat?: number[]; + lngLat?: [number, number]; testimonial?: { text: string; name?: string; @@ -51,7 +52,7 @@ function augmentInfo(item: Item) { institutionType: item.fields.type, fields: item.fields }; - const lngLat = [Number(item.fields.long), Number(item.fields.lat)]; + const lngLat: [number, number] = [Number(item.fields.long), Number(item.fields.lat)]; if (!(lngLat[0] === 0 && lngLat[1] === 0)) { result.lngLat = lngLat; diff --git a/src/app/pages/separatemap/map-api.js b/src/app/pages/separatemap/map-api.tsx similarity index 72% rename from src/app/pages/separatemap/map-api.js rename to src/app/pages/separatemap/map-api.tsx index e36603f08..2ab7ec276 100644 --- a/src/app/pages/separatemap/map-api.js +++ b/src/app/pages/separatemap/map-api.tsx @@ -1,12 +1,12 @@ -import mapboxgl from 'mapbox-gl'; +import mapboxgl, { FilterSpecification, LngLatBoundsLike, LngLatLike, MapOptions } from 'mapbox-gl'; import mapboxPromise from '~/models/mapbox'; - -const settings = window.SETTINGS; +import settings from '~/helpers/window-settings'; +import { AugmentedInfo } from '~/models/query-schools'; // Set up CSS once, when needed (() => { const cssEl = Object.assign( - document.createElement('link'), + document.createElement('link'), { rel: 'stylesheet', href: 'https://api.tiles.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.css', @@ -16,16 +16,16 @@ const settings = window.SETTINGS; const firstLink = document.querySelector('head link[rel="stylesheet"]') || document.querySelector('head title'); - firstLink?.parentNode.insertBefore(cssEl, firstLink.nextSibling); + firstLink?.parentNode?.insertBefore(cssEl, firstLink.nextSibling); })(); -mapboxgl.accessToken = settings.mapboxPK; +mapboxgl.accessToken = settings().mapboxPK; -function hasLngLat({lngLat}) { +function hasLngLat({lngLat}: {lngLat?: unknown}) { return Boolean(lngLat); } -async function createMapboxMap(mapOptions) { +async function createMapboxMap(mapOptions: MapOptions) { const mapbox = await mapboxPromise; return new mapboxgl.Map({ @@ -34,7 +34,7 @@ async function createMapboxMap(mapOptions) { }); } -export default function createMap(options) { +export default function createMap(options: MapOptions) { // This is a promise that yields the MapboxGL map object const loaded = createMapboxMap(options); const initialState = { @@ -42,7 +42,7 @@ export default function createMap(options) { zoom: options.zoom }; - function setBounds(bounds) { + function setBounds(bounds?: LngLatBoundsLike) { loaded.then((map) => { if (bounds) { map.fitBounds(bounds, { @@ -55,7 +55,7 @@ export default function createMap(options) { }); } - function setFilters(filterSpec) { + function setFilters(filterSpec?: FilterSpecification) { loaded.then((map) => { map.setFilter('os-schools', filterSpec); map.setFilter('os-schools-shadow-at-8', filterSpec); @@ -74,9 +74,11 @@ export default function createMap(options) { tooltip.remove(); } - function showPoints(pointList) { - const mappable = ({lngLat: [lng, lat]}) => !(lng === 0 && lat === 0); - const mappableData = pointList.filter(hasLngLat).filter(mappable); + type HasLngLat = AugmentedInfo & Required> + + function showPoints(pointList: AugmentedInfo[]) { + const mappable = ({lngLat: [lng, lat]}: HasLngLat) => !(lng === 0 && lat === 0); + const mappableData = (pointList.filter(hasLngLat) as HasLngLat[]).filter(mappable); tooltip.remove(); if (mappableData.length === 0) { @@ -93,13 +95,13 @@ export default function createMap(options) { } } - function showTooltip(schoolInfo, flyThere) { + function showTooltip(schoolInfo: AugmentedInfo) { if (!hasLngLat(schoolInfo)) { return; } let html = `
-
@@ -109,14 +111,11 @@ export default function createMap(options) { if (schoolInfo.cityState) { html += `
${schoolInfo.cityState}`; } - tooltip.setLngLat(schoolInfo.lngLat); + tooltip.setLngLat(schoolInfo.lngLat as LngLatLike); tooltip.setHTML(html); loaded.then((map) => { tooltip.addTo(map); }); - if (flyThere) { - setBounds((new mapboxgl.LngLatBounds()).extend(schoolInfo.lngLat)); - } } loaded.then( diff --git a/src/app/pages/separatemap/map-context.js b/src/app/pages/separatemap/map-context.tsx similarity index 56% rename from src/app/pages/separatemap/map-context.js rename to src/app/pages/separatemap/map-context.tsx index bd34427aa..8ff884f9c 100644 --- a/src/app/pages/separatemap/map-context.js +++ b/src/app/pages/separatemap/map-context.tsx @@ -1,13 +1,14 @@ import React from 'react'; import buildContext from '~/components/jsx-helpers/build-context'; -import Map from './map-api'; +import createMap from './map-api'; import {isMobileDisplay} from '~/helpers/device'; -import {queryById} from '~/models/query-schools'; +import {AugmentedInfo, queryById} from '~/models/query-schools'; +import {assertNotNull} from '~/helpers/data'; -function useMap(id) { +function useMap(id: string) { const mapZoom = isMobileDisplay() ? 2 : 3; const map = React.useMemo( - () => new Map({ + () => createMap({ container: id, center: [-95.712891, 37.090240], zoom: mapZoom, @@ -19,9 +20,9 @@ function useMap(id) { ); React.useEffect(() => { - const container = document.getElementById('mapd'); - const clickHandler = (event) => { - const delegateTarget = event.target.closest('.mapboxgl-popup-content .put-away'); + const container = assertNotNull(document.getElementById('mapd')); + const clickHandler = (event: MouseEvent) => { + const delegateTarget = (event.target as Element).closest('.mapboxgl-popup-content .put-away'); if (delegateTarget) { map.tooltip.remove(); @@ -35,30 +36,34 @@ function useMap(id) { return map; } -function useSelectedSchool(map) { - const [selectedSchool, setSelectedSchool] = React.useState(); +type Map = ReturnType; + +function useSelectedSchool(map: Map) { + const [selectedSchool, setSelectedSchool] = React.useState(null); const selectSchool = React.useCallback( - (id) => queryById(id).then( + (id: string) => queryById(id).then( (schoolInfo) => setSelectedSchool(schoolInfo) ), [] ); React.useEffect( - () => map.loaded.then((glMap) => { - glMap.on( - 'click', - 'os-schools', - (el) => selectSchool(el.features[0].properties.id) - ); - }), + () => { + map.loaded.then((glMap) => { + glMap.on( + 'click', + 'os-schools', + (el) => selectSchool(el.features?.[0].properties?.id) + ); + }); + }, [map.loaded, selectSchool] ); return selectedSchool; } -function useContextValue({id}) { +function useContextValue({id}: {id: string}) { const map = useMap(id); const selectedSchool = useSelectedSchool(map); diff --git a/src/app/pages/separatemap/search-box/filters/filters.js b/src/app/pages/separatemap/search-box/filters/filters.tsx similarity index 59% rename from src/app/pages/separatemap/search-box/filters/filters.js rename to src/app/pages/separatemap/search-box/filters/filters.tsx index 8053a9617..b6bbfb805 100644 --- a/src/app/pages/separatemap/search-box/filters/filters.js +++ b/src/app/pages/separatemap/search-box/filters/filters.tsx @@ -1,33 +1,34 @@ -import React from 'react'; +import React, { ChangeEvent } from 'react'; import FormSelect from '~/components/form-select/form-select'; +import type {SetHandle} from '~/helpers/data'; import './filters.scss'; -function InstitutionSelector({setInstitution}) { - const options = [ - {label: 'Any', value: '', selected: true}, - {label: 'College/University', value: 'College/University'}, - {label: 'Technical/Community College', value: 'Technical/Community College'}, - {label: 'High School', value: 'High School'} - ]; - const onChange = React.useCallback( - (event) => setInstitution(event.target.value), - [setInstitution] - ); +const options = [ + {label: 'Any', value: '', selected: true}, + {label: 'College/University', value: 'College/University'}, + {label: 'Technical/Community College', value: 'Technical/Community College'}, + {label: 'High School', value: 'High School'} +]; +function InstitutionSelector({setInstitution}: {setInstitution: React.Dispatch>}) { return ( ); } -function ForCheckbox({name, label, selected}) { +function ForCheckbox({name, label, selected}: { + name: string; + label: string; + selected: SetHandle; +}) { const onChange = React.useCallback( - (event) => { - const {checked} = event.target; + (event: ChangeEvent) => { + const {checked} = event.target as HTMLInputElement; if (checked) { selected.add(name); @@ -46,7 +47,10 @@ function ForCheckbox({name, label, selected}) { ); } -export default function Filters({selected, setInstitution}) { +export default function Filters({selected, setInstitution}: { + selected: SetHandle; + setInstitution: React.Dispatch>; +}) { return (
diff --git a/src/app/pages/separatemap/search-box/inputs/inputs.scss b/src/app/pages/separatemap/search-box/inputs/inputs.scss index 3f6318f6a..e28b7be60 100644 --- a/src/app/pages/separatemap/search-box/inputs/inputs.scss +++ b/src/app/pages/separatemap/search-box/inputs/inputs.scss @@ -47,7 +47,10 @@ } .search-clear { - cursor: pointer; + appearance: none; + border: 0; + box-shadow: none; + padding: 0; &[hidden] { display: none; diff --git a/src/app/pages/separatemap/search-box/inputs/inputs.js b/src/app/pages/separatemap/search-box/inputs/inputs.tsx similarity index 67% rename from src/app/pages/separatemap/search-box/inputs/inputs.js rename to src/app/pages/separatemap/search-box/inputs/inputs.tsx index b1d4305e6..edd9a469f 100644 --- a/src/app/pages/separatemap/search-box/inputs/inputs.js +++ b/src/app/pages/separatemap/search-box/inputs/inputs.tsx @@ -7,7 +7,18 @@ import {faChevronLeft} from '@fortawesome/free-solid-svg-icons/faChevronLeft'; import cn from 'classnames'; import './inputs.scss'; -function FilterToggleButton({filtersHidden, toggleFilters, children}) { +type CommonViewProps = { + toggle: () => void; + minimized: boolean; + filtersHidden: boolean; + toggleFilters: () => void; + searchValue: string; + setSearchValue: (s: string) => void; +} + +function FilterToggleButton({filtersHidden, toggleFilters, children}: React.PropsWithChildren< + Pick +>) { return (
); } -function SearchIcon({minimized, toggle}) { +function SearchIcon({minimized, toggle}: Pick) { const icon = minimized ? faSearch : faChevronLeft; return (
toggle()} + tabIndex={0} onClick={() => toggle()} + aria-label="toggle search window" >
@@ -66,8 +86,11 @@ function SearchIcon({minimized, toggle}) { function InputView({ className, placeholder, children, - minimized, toggle, searchValue, setSearchValue, filtersHidden, toggleFilters -}) { + toggle, minimized, filtersHidden, toggleFilters, searchValue, setSearchValue +}: React.PropsWithChildren<{ + className: string; + placeholder: string; +} & CommonViewProps>) { return (
@@ -82,14 +105,8 @@ function InputView({ ); } -export default function Inputs({ - toggle, minimized, filtersHidden, toggleFilters, searchValue, setSearchValue -}) { - const commonViewProps = { - minimized, toggle, searchValue, setSearchValue, - filtersHidden, toggleFilters - }; +export default function Inputs(commonViewProps: CommonViewProps) { return (
['testimonial']}) { return (
@@ -28,7 +29,7 @@ function Testimonial({testimonial}) { ); } -function SchoolDetails({model}) { +function SchoolDetails({model}: {model: AugmentedInfo}) { const [ savingsTotal, savingsThisYear, testimonial ] = [ @@ -49,9 +50,12 @@ function SchoolDetails({model}) { ); } -export default function ResultBox({model, theOpenOne, setTheOpenOne}) { - const ref = useRef(); - const [name, location] = [model.fields.name, model.cityState]; +export default function ResultBox({model, theOpenOne, setTheOpenOne}: { + model: AugmentedInfo; + theOpenOne: AugmentedInfo | null; + setTheOpenOne: (m: AugmentedInfo | null) => void; +}) { + const ref = useRef(null); const isOpen = theOpenOne === model; function toggle() { @@ -60,7 +64,7 @@ export default function ResultBox({model, theOpenOne, setTheOpenOne}) { useLayoutEffect(() => { if (isOpen) { - ref.current.scrollIntoView({block: 'nearest', behavior: 'smooth'}); + ref.current?.scrollIntoView({block: 'nearest', behavior: 'smooth'}); } }); @@ -71,8 +75,8 @@ export default function ResultBox({model, theOpenOne, setTheOpenOne}) { onClick={toggle} >
-

{name}

-
{location}
+

{model.fields.name}

+
{model.cityState}
diff --git a/src/app/pages/separatemap/search-box/results.js b/src/app/pages/separatemap/search-box/results.tsx similarity index 56% rename from src/app/pages/separatemap/search-box/results.js rename to src/app/pages/separatemap/search-box/results.tsx index e067209a4..57d0beb8c 100644 --- a/src/app/pages/separatemap/search-box/results.js +++ b/src/app/pages/separatemap/search-box/results.tsx @@ -1,16 +1,19 @@ import {useState, useEffect} from 'react'; -import querySchools from '~/models/query-schools'; +import querySchools, {AugmentedInfo} from '~/models/query-schools'; import {useDataFromPromise} from '~/helpers/page-data-utils'; +import type {SetHandle} from '~/helpers/data'; -function useSchoolsPromise(searchString, filters, institution) { - const [promise, setPromise] = useState(); +type Results = {TOO_MANY: unknown} | AugmentedInfo[]; + +function useSchoolsPromise(searchString: string, filters: SetHandle, institution: string) { + const [promise, setPromise] = useState | null>(null); useEffect(() => { - const filtersArray = Array.from(filters.values()); + const filtersArray = Array.from(filters.values()) as string[]; const filtersDict = filtersArray.reduce((a, b) => { a[b] = b; return a; - }, {}); + }, {} as Record); filtersDict['institution-type'] = institution; const p = querySchools(searchString, filtersDict); @@ -24,19 +27,19 @@ function useSchoolsPromise(searchString, filters, institution) { } // eslint-disable-next-line complexity -export default function useResults(searchString, selectedFilters, institution) { +export default function useResults(searchString: string, selectedFilters: SetHandle, institution: string) { const nothingSelected = searchString === '' && Array.from(selectedFilters.values()).length === 0; const schoolsPromise = useSchoolsPromise(searchString, selectedFilters, institution); - const results = useDataFromPromise(schoolsPromise); + const results = useDataFromPromise(schoolsPromise); let message = null; if (results && !nothingSelected) { - if (results.TOO_MANY) { + if ('TOO_MANY' in results) { message = 'Too many matching results'; } else if (results.length === 0) { message = 'No matching results'; } } - return [results || [], message]; + return [results || [], message] as [Results, string]; } diff --git a/src/app/pages/separatemap/search-box/search-box.js b/src/app/pages/separatemap/search-box/search-box.tsx similarity index 80% rename from src/app/pages/separatemap/search-box/search-box.js rename to src/app/pages/separatemap/search-box/search-box.tsx index 270e4aef1..ffb99a0f6 100644 --- a/src/app/pages/separatemap/search-box/search-box.js +++ b/src/app/pages/separatemap/search-box/search-box.tsx @@ -7,6 +7,7 @@ import ResultBox from './result-box/result-box'; import useResults from './results'; import useMapContext from '../map-context'; import './search-box.scss'; +import {AugmentedInfo} from '~/models/query-schools'; /* When you select school, see if it is in results. @@ -14,13 +15,13 @@ If so, setTheOpenOne to it and scroll to it. Otherwise, put it in as the sole element of the list */ -function useTheOpenOne({results}) { - const [theOpenOne, setTheOpenOne] = useState(); +function useTheOpenOne({results}: {results: AugmentedInfo[]}) { + const [theOpenOne, setTheOpenOne] = useState(null); const {map, selectedSchool} = useMapContext(); useEffect(() => { if (selectedSchool) { - setTheOpenOne(results.find((r) => r.pk === selectedSchool.pk)); + setTheOpenOne(results.find((r) => r.pk === selectedSchool.pk) ?? null); map.showTooltip(selectedSchool); } else { setTheOpenOne(null); @@ -43,14 +44,17 @@ function useTheOpenOne({results}) { [map, theOpenOne, results] ); - return [theOpenOne, setTheOpenOne]; + return [theOpenOne, setTheOpenOne] as const; } -function SearchResults({minimized, results=[]}) { +function SearchResults({minimized, results}: { + minimized: boolean; + results: AugmentedInfo[]; +}) { const {selectedSchool} = useMapContext(); const showSelectedSchool = Boolean(results.length < 1 && selectedSchool); const resultsOrSchool = React.useMemo( - () => showSelectedSchool ? [selectedSchool] : results, + () => showSelectedSchool ? [selectedSchool as AugmentedInfo] : results, [showSelectedSchool, selectedSchool, results] ); const resultsHidden = resultsOrSchool.length < 1; @@ -61,7 +65,7 @@ function SearchResults({minimized, results=[]}) { { resultsOrSchool?.map((school) => ) @@ -70,7 +74,10 @@ function SearchResults({minimized, results=[]}) { ); } -export default function SearchBox({minimized, toggle}) { +export default function SearchBox({minimized, toggle}: { + minimized: boolean; + toggle: () => void; +}) { const [searchValue, setSearchValue] = useState(''); const [filtersHidden, toggleFilters] = useToggle(true); const selectedFilters = useSet(); @@ -91,7 +98,7 @@ export default function SearchBox({minimized, toggle}) { - + {results instanceof Array && }
); } diff --git a/src/app/pages/separatemap/separatemap.js b/src/app/pages/separatemap/separatemap.tsx similarity index 94% rename from src/app/pages/separatemap/separatemap.js rename to src/app/pages/separatemap/separatemap.tsx index 0b4e8dd2b..0f11045ba 100644 --- a/src/app/pages/separatemap/separatemap.js +++ b/src/app/pages/separatemap/separatemap.tsx @@ -40,7 +40,9 @@ function PopupMessage() {
togglePopup()} />
From 91b97288bc8b82802858f52e6825d0fffd95be75 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Thu, 28 Aug 2025 16:06:53 -0500 Subject: [PATCH 2/3] Tests --- test/src/pages/separatemap/filters.test.js | 18 -- test/src/pages/separatemap/map-api.test.ts | 122 +++++++++++ test/src/pages/separatemap/result-box.test.js | 40 ---- .../pages/separatemap/separatemap.test.tsx | 202 ++++++++++++++++++ 4 files changed, 324 insertions(+), 58 deletions(-) delete mode 100644 test/src/pages/separatemap/filters.test.js create mode 100644 test/src/pages/separatemap/map-api.test.ts delete mode 100644 test/src/pages/separatemap/result-box.test.js create mode 100644 test/src/pages/separatemap/separatemap.test.tsx diff --git a/test/src/pages/separatemap/filters.test.js b/test/src/pages/separatemap/filters.test.js deleted file mode 100644 index 280652961..000000000 --- a/test/src/pages/separatemap/filters.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, {useState} from 'react'; -import {render, screen} from '@testing-library/preact'; -import Filters from '~/pages/separatemap/search-box/filters/filters'; -import {useSet} from '~/helpers/data'; - -// I need the hooks -function WrappedFilters() { - const selectedFilters = useSet(); - const [institution, setInstitution] = useState(''); - - return ( - - ); -} -it('creates', () => { - render(); - expect(screen.getAllByRole('checkbox')).toHaveLength(3); -}); diff --git a/test/src/pages/separatemap/map-api.test.ts b/test/src/pages/separatemap/map-api.test.ts new file mode 100644 index 000000000..98ac50248 --- /dev/null +++ b/test/src/pages/separatemap/map-api.test.ts @@ -0,0 +1,122 @@ +import createMap from '~/pages/separatemap/map-api'; +import {waitFor} from '@testing-library/preact'; +import type {AugmentedInfo} from '~/models/query-schools'; + +const triggers: Record void> = {}; +let mockLoaded: jest.Mock; +let mockResize: jest.Mock; +let mockSetFilter: jest.Mock; + +jest.mock('~/models/mapbox', () => () => console.info('*** LOADED?')); +jest.mock('mapbox-gl', () => { + const functionWithThings = () => ({}); + const mockMap = { + on: (...args: unknown[]) => { + const fn = args.pop() as () => void; + + (args as string[]).forEach((t) => { + triggers[t] = fn; + }); + }, + loaded: (mockLoaded = jest.fn().mockReturnValue(true)), + resize: (mockResize = jest.fn()), + getCanvas: () => ({style: {}}), + setFilter: (mockSetFilter = jest.fn()), + easeTo: jest.fn(), + fitBounds: jest.fn() + }; + + functionWithThings.Map = jest.fn().mockReturnValue(mockMap); + functionWithThings.Popup = jest.fn().mockReturnValue({ + remove: jest.fn() + }); + functionWithThings.LngLatBounds = jest.fn().mockReturnValue({ + extend: jest.fn().mockReturnValue([0, 10]) + }); + + return { + __esModule: true, + default: functionWithThings + }; +}); + +describe('map-api', () => { + it('sets up event handlers', async () => { + const container = document.createElement('div'); + const {tooltip} = createMap({container}); + + await waitFor(() => expect(triggers.load).toBeTruthy()); + triggers.load(); + expect(mockLoaded).toHaveBeenCalled(); + expect(mockResize).toHaveBeenCalled(); + triggers.mouseenter(); + triggers.mouseleave(); + triggers.click({}); + expect(tooltip.remove).toHaveBeenCalled(); + (tooltip.remove as jest.Mock).mockReset(); + triggers.click({features: true}); + expect(tooltip.remove).not.toHaveBeenCalled(); + }); + it('shows tooltip for school', async () => { + const container = document.createElement('div'); + const {loaded, tooltip, showTooltip} = createMap({container}); + const schoolInfo = { + lngLat: [1, 2], + fields: { + name: 'Test University' + }, + cityState: 'Smallville, KS' + } as unknown as AugmentedInfo; + + await loaded; + tooltip.setLngLat = jest.fn(); + tooltip.setHTML = jest.fn(); + tooltip.addTo = jest.fn(); + showTooltip(schoolInfo); + expect(tooltip.setLngLat).toHaveBeenCalledWith(schoolInfo.lngLat); + expect(tooltip.setHTML).toHaveBeenCalledWith( + expect.stringContaining(schoolInfo.cityState) + ); + // Handles empty cityState + schoolInfo.cityState = ''; + showTooltip(schoolInfo); + expect(tooltip.setHTML).toHaveBeenCalledWith( + expect.not.stringMatching('
') + ); + // Does not display if no lngLat + jest.clearAllMocks(); + delete schoolInfo.lngLat; + showTooltip(schoolInfo); + expect(tooltip.setLngLat).not.toHaveBeenCalled(); + expect(tooltip.setHTML).not.toHaveBeenCalled(); + }); + it('shows points of schools', async () => { + const container = document.createElement('div'); + const {loaded, showPoints} = createMap({container}); + const schoolInfo = { + lngLat: [1, 2], + fields: { + name: 'Test University' + }, + cityState: 'Smallville, KS' + } as unknown as AugmentedInfo; + + await loaded; + // Handles empty list (lnglat of 0, 0 gets filtered out) + showPoints([{...schoolInfo, lngLat: [0, 0]}]); + await waitFor(() => + expect(mockSetFilter).toHaveBeenCalledWith('os-schools', undefined) + ); + mockSetFilter.mockClear(); + // Non-empty + showPoints([schoolInfo]); + await waitFor(() => + expect(mockSetFilter).toHaveBeenCalledWith('os-schools', [ + 'in', + 'id', + undefined + ]) + ); + mockSetFilter.mockClear(); + }); +}); diff --git a/test/src/pages/separatemap/result-box.test.js b/test/src/pages/separatemap/result-box.test.js deleted file mode 100644 index 89945c644..000000000 --- a/test/src/pages/separatemap/result-box.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import {render, screen} from '@testing-library/preact'; -import userEvent from '@testing-library/user-event'; -import ResultBox from '~/pages/separatemap/search-box/result-box/result-box'; - -const model = { - cityState: "Pomfret, Maryland", - fields: { - salesforce_id: "0016f00002alpsuAAA", - name: "Maurice J. McDonough High School", - phone: "(301) 934-2944", - website: "https://www.ccboe.com/schools/mcdonough/", - type: "High School" - }, - institutionType: "High School", - institutionalPartner: false, - lngLat: (2) [-77.034, 38.555], - pk: 656595, - testimonial: { - text: 'Good stuff', - name: 'Some Body', - position: 'Chief Example' - } -}; -function Wrapper() { - const [theOpenOne, setTheOpenOne] = React.useState(); - - return ( - - ); -} - -it('opens on click of toggle', async () => { - const user = userEvent.setup(); - - render(); - expect(screen.queryAllByText('savings', {exact: false})).toHaveLength(0); - await user.click(screen.getByRole('switch')); - expect(screen.queryAllByText('savings', {exact: false})).toHaveLength(1); -}); diff --git a/test/src/pages/separatemap/separatemap.test.tsx b/test/src/pages/separatemap/separatemap.test.tsx new file mode 100644 index 000000000..615a9ee10 --- /dev/null +++ b/test/src/pages/separatemap/separatemap.test.tsx @@ -0,0 +1,202 @@ +import React from 'react'; +import {render, screen, waitFor} from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import SeparateMap from '~/pages/separatemap/separatemap'; +import * as DH from '~/helpers/use-document-head'; +import * as MD from '~/helpers/device'; + +const mockMapbox = jest.fn(); +const mockQuerySchools = jest.fn(); +const mockQueryById = jest.fn(); +let mockOn: jest.Mock; +let mockRemove: jest.Mock; + +jest.spyOn(DH, 'default').mockReturnValue(); +const mockIsMobileDisplay = jest + .spyOn(MD, 'isMobileDisplay') + .mockReturnValue(false); + +jest.mock('~/models/mapbox', () => () => mockMapbox()); +jest.mock('mapbox-gl', () => { + const functionWithThings = () => ({}); + + functionWithThings.Map = jest.fn().mockReturnValue({ + on: (mockOn = jest.fn()), + setFilter: jest.fn(), + fitBounds: jest.fn() + }); + functionWithThings.Popup = jest.fn().mockReturnValue({ + remove: (mockRemove = jest.fn()), + setLngLat: jest.fn(), + setHTML: jest.fn(), + addTo: jest.fn() + }); + functionWithThings.LngLatBounds = jest.fn().mockReturnValue({ + extend: jest.fn().mockReturnValue([0, 10]) + }); + + return { + __esModule: true, + default: functionWithThings + }; +}); +jest.mock('~/models/query-schools', () => ({ + __esModule: true, + default: () => mockQuerySchools(), + queryById: () => mockQueryById() +})); +jest.mock('~/helpers/main-class-hooks', () => ({ + __esModule: true, + useMainModal: jest.fn() +})); + +const aSchoolData = { + cityState: 'Pomfret, Maryland', + fields: { + // eslint-disable-next-line camelcase + salesforce_id: '0016f00002alpsuAAA', + name: 'Maurice J. McDonough High School', + phone: '(301) 934-2944', + website: 'https://www.ccboe.com/schools/mcdonough/', + type: 'High School' + }, + institutionType: 'High School', + institutionalPartner: false, + lngLat: [-77.034, 38.555], + pk: '656595', + testimonial: { + text: 'Good stuff', + name: 'Some Body', + position: 'Chief Example' + } +}; + +describe('separatemap', () => { + const user = userEvent.setup(); + + mockMapbox.mockResolvedValue({ + name: 'mock-schools', + style: 'mapbox-style' + }); + mockQuerySchools.mockResolvedValue({TOO_MANY: true}); + + it('renders; popup dismisses', async () => { + render(); + await user.click( + await screen.findByRole('button', {name: 'close popup'}) + ); + expect(screen.queryByRole('button', {name: 'close popup'})).toBeNull(); + }); + it('handles school click', async () => { + mockOn.mockImplementation( + (eType: string, what: string, fn: (el: object) => void) => { + if (eType === 'click' && what === 'os-schools') { + fn({ + features: [ + { + properties: { + id: 1 + } + } + ] + }); + } + } + ); + mockQueryById.mockResolvedValue(null); + // cover mapZoom branch + mockIsMobileDisplay.mockReturnValue(true); + render(); + await waitFor(() => expect(mockQueryById).toHaveBeenCalled()); + + const mapd = document.getElementById('mapd') as HTMLElement; + + mapd.innerHTML = `
+
+ +
+
`; + + await user.click(screen.getByRole('button', {name: 'Test put-away'})); + expect(mockRemove).toHaveBeenCalled(); + mockRemove.mockClear(); + await user.click(mapd); + expect(mockRemove).not.toHaveBeenCalled(); + }); + it('interacts with search box', async () => { + mockQuerySchools.mockResolvedValue([aSchoolData]); + + render(); + const inputs = await screen.findAllByRole('textbox'); + + await user.type(inputs[0], 'Test{enter}'); + await user.click(screen.getByRole('switch')); + }); + it('handles too many search results', async () => { + mockQuerySchools.mockResolvedValue({TOO_MANY: true}); + + render(); + const inputs = await screen.findAllByRole('textbox'); + + await user.type(inputs[0], 'Test{enter}'); + expect(screen.queryByRole('switch')).toBeNull(); + }); + it('handles filter checkboxes; school query returns no promise', async () => { + mockQueryById.mockClear(); + mockQuerySchools.mockReturnValue(null); + render(); + await screen.findAllByRole('textbox'); + await user.click(screen.getByRole('button', {name: 'Filter by'})); + await user.click(screen.getAllByRole('checkbox')[0]); + expect(mockQueryById).toHaveBeenCalledTimes(1); + // Exercises checkbox unselect code in filters.tsx + await user.click(screen.getAllByRole('checkbox')[0]); + // Exercise institution selector + const select = screen.getByRole('combobox'); + + expect(select.textContent).toBe('Any'); + await user.click(select); + await user.click(screen.getByRole('option', {name: 'High School'})); + expect(select.textContent).toBe('High School'); + }); + it('handles school query returning no schools', async () => { + mockQuerySchools.mockResolvedValue([]); + mockQueryById.mockResolvedValue(null); + render(); + const inputs = await screen.findAllByRole('textbox'); + + await user.type(inputs[0], 'Test{enter}'); + expect(screen.queryByRole('switch')).toBeNull(); + }); + it('handles school match and when all schools are filtered out', async () => { + mockQuerySchools.mockResolvedValue([aSchoolData]); + mockQueryById.mockResolvedValue(aSchoolData); + render(); + const inputs = await screen.findAllByRole('textbox'); + + await user.type(inputs[0], 'Test{enter}'); + mockQuerySchools.mockResolvedValue([]); + // select/open/fly-to + await user.click(screen.getByRole('switch')); + await user.type(inputs[0], 'Oops{enter}'); + // unselect/close/fly-out-from + await user.click(screen.getByRole('switch')); + await user.click( + screen.getAllByRole('button', {name: 'clear search'})[0] + ); + // Clear search button goes away when search is cleared + expect(screen.queryByRole('button', {name: 'clear search'})).toBeNull(); + }); + it('toggles search window open/closed', async () => { + render(); + const toggle = screen.getAllByRole('button', { + name: 'toggle search window' + })[0]; + + expect(toggle.getAttribute('aria-pressed')).toBe('false'); + await user.click(toggle); + expect(toggle.getAttribute('aria-pressed')).toBe('true'); + await user.click(toggle); + expect(toggle.getAttribute('aria-pressed')).toBe('false'); + }); +}); From 8e617a02023e1fde172875b9e8c79c845b069a3e Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Thu, 28 Aug 2025 16:22:07 -0500 Subject: [PATCH 3/3] Prettier source files --- src/app/pages/separatemap/map-api.tsx | 70 +++++++------ src/app/pages/separatemap/map-context.tsx | 53 +++++----- .../search-box/filters/filters.tsx | 35 +++++-- .../separatemap/search-box/inputs/inputs.tsx | 66 +++++++++---- .../search-box/result-box/result-box.tsx | 36 ++++--- .../pages/separatemap/search-box/results.tsx | 32 ++++-- .../separatemap/search-box/search-box.tsx | 99 ++++++++++++------- src/app/pages/separatemap/separatemap.tsx | 49 ++++----- 8 files changed, 273 insertions(+), 167 deletions(-) diff --git a/src/app/pages/separatemap/map-api.tsx b/src/app/pages/separatemap/map-api.tsx index 2ab7ec276..da54e770a 100644 --- a/src/app/pages/separatemap/map-api.tsx +++ b/src/app/pages/separatemap/map-api.tsx @@ -1,19 +1,22 @@ -import mapboxgl, { FilterSpecification, LngLatBoundsLike, LngLatLike, MapOptions } from 'mapbox-gl'; +import mapboxgl, { + FilterSpecification, + LngLatBoundsLike, + LngLatLike, + MapOptions +} from 'mapbox-gl'; import mapboxPromise from '~/models/mapbox'; import settings from '~/helpers/window-settings'; -import { AugmentedInfo } from '~/models/query-schools'; +import {AugmentedInfo} from '~/models/query-schools'; // Set up CSS once, when needed (() => { - const cssEl = Object.assign( - document.createElement('link'), - { - rel: 'stylesheet', - href: 'https://api.tiles.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.css', - type: 'text/css' - } - ); - const firstLink = document.querySelector('head link[rel="stylesheet"]') || + const cssEl = Object.assign(document.createElement('link'), { + rel: 'stylesheet', + href: 'https://api.tiles.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.css', + type: 'text/css' + }); + const firstLink = + document.querySelector('head link[rel="stylesheet"]') || document.querySelector('head title'); firstLink?.parentNode?.insertBefore(cssEl, firstLink.nextSibling); @@ -74,11 +77,14 @@ export default function createMap(options: MapOptions) { tooltip.remove(); } - type HasLngLat = AugmentedInfo & Required> + type HasLngLat = AugmentedInfo & Required>; function showPoints(pointList: AugmentedInfo[]) { - const mappable = ({lngLat: [lng, lat]}: HasLngLat) => !(lng === 0 && lat === 0); - const mappableData = (pointList.filter(hasLngLat) as HasLngLat[]).filter(mappable); + const mappable = ({lngLat: [lng, lat]}: HasLngLat) => + !(lng === 0 && lat === 0); + const mappableData = ( + pointList.filter(hasLngLat) as HasLngLat[] + ).filter(mappable); tooltip.remove(); if (mappableData.length === 0) { @@ -88,7 +94,11 @@ export default function createMap(options: MapOptions) { (bound, obj) => bound.extend(obj.lngLat), new mapboxgl.LngLatBounds() ); - const filterSpec = ['in', 'id', ...(mappableData.map((obj) => obj.pk))]; + const filterSpec = [ + 'in', + 'id', + ...mappableData.map((obj) => obj.pk) + ]; setFilters(filterSpec); setBounds(bounds); @@ -118,22 +128,20 @@ export default function createMap(options: MapOptions) { }); } - loaded.then( - (map) => { - map.on('load', () => map.loaded() && map.resize()); - map.on('mouseenter', 'os-schools', () => { - map.getCanvas().style.cursor = 'pointer'; - }); - map.on('mouseleave', 'os-schools', () => { - map.getCanvas().style.cursor = ''; - }); - map.on('click', (el) => { - if (!el.features && tooltip) { - tooltip.remove(); - } - }); - } - ); + loaded.then((map) => { + map.on('load', () => map.loaded() && map.resize()); + map.on('mouseenter', 'os-schools', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + map.on('mouseleave', 'os-schools', () => { + map.getCanvas().style.cursor = ''; + }); + map.on('click', (el) => { + if (!el.features && tooltip) { + tooltip.remove(); + } + }); + }); return { loaded, diff --git a/src/app/pages/separatemap/map-context.tsx b/src/app/pages/separatemap/map-context.tsx index 8ff884f9c..5a7ac239d 100644 --- a/src/app/pages/separatemap/map-context.tsx +++ b/src/app/pages/separatemap/map-context.tsx @@ -8,21 +8,24 @@ import {assertNotNull} from '~/helpers/data'; function useMap(id: string) { const mapZoom = isMobileDisplay() ? 2 : 3; const map = React.useMemo( - () => createMap({ - container: id, - center: [-95.712891, 37.090240], - zoom: mapZoom, - pitchWithRotate: false, - dragRotate: false, - touchZoomRotate: false - }), + () => + createMap({ + container: id, + center: [-95.712891, 37.09024], + zoom: mapZoom, + pitchWithRotate: false, + dragRotate: false, + touchZoomRotate: false + }), [mapZoom, id] ); React.useEffect(() => { const container = assertNotNull(document.getElementById('mapd')); const clickHandler = (event: MouseEvent) => { - const delegateTarget = (event.target as Element).closest('.mapboxgl-popup-content .put-away'); + const delegateTarget = (event.target as Element).closest( + '.mapboxgl-popup-content .put-away' + ); if (delegateTarget) { map.tooltip.remove(); @@ -39,26 +42,21 @@ function useMap(id: string) { type Map = ReturnType; function useSelectedSchool(map: Map) { - const [selectedSchool, setSelectedSchool] = React.useState(null); + const [selectedSchool, setSelectedSchool] = + React.useState(null); const selectSchool = React.useCallback( - (id: string) => queryById(id).then( - (schoolInfo) => setSelectedSchool(schoolInfo) - ), + (id: string) => + queryById(id).then((schoolInfo) => setSelectedSchool(schoolInfo)), [] ); - React.useEffect( - () => { - map.loaded.then((glMap) => { - glMap.on( - 'click', - 'os-schools', - (el) => selectSchool(el.features?.[0].properties?.id) - ); - }); - }, - [map.loaded, selectSchool] - ); + React.useEffect(() => { + map.loaded.then((glMap) => { + glMap.on('click', 'os-schools', (el) => + selectSchool(el.features?.[0].properties?.id) + ); + }); + }, [map.loaded, selectSchool]); return selectedSchool; } @@ -75,7 +73,4 @@ function useContextValue({id}: {id: string}) { const {useContext, ContextProvider} = buildContext({useContextValue}); -export { - useContext as default, - ContextProvider as MapContextProvider -}; +export {useContext as default, ContextProvider as MapContextProvider}; diff --git a/src/app/pages/separatemap/search-box/filters/filters.tsx b/src/app/pages/separatemap/search-box/filters/filters.tsx index b6bbfb805..bb18d5d0f 100644 --- a/src/app/pages/separatemap/search-box/filters/filters.tsx +++ b/src/app/pages/separatemap/search-box/filters/filters.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent } from 'react'; +import React, {ChangeEvent} from 'react'; import FormSelect from '~/components/form-select/form-select'; import type {SetHandle} from '~/helpers/data'; import './filters.scss'; @@ -6,14 +6,21 @@ import './filters.scss'; const options = [ {label: 'Any', value: '', selected: true}, {label: 'College/University', value: 'College/University'}, - {label: 'Technical/Community College', value: 'Technical/Community College'}, + { + label: 'Technical/Community College', + value: 'Technical/Community College' + }, {label: 'High School', value: 'High School'} ]; -function InstitutionSelector({setInstitution}: {setInstitution: React.Dispatch>}) { +function InstitutionSelector({ + setInstitution +}: { + setInstitution: React.Dispatch>; +}) { return ( >; }) { @@ -57,15 +71,18 @@ export default function Filters({selected, setInstitution}: {
diff --git a/src/app/pages/separatemap/search-box/inputs/inputs.tsx b/src/app/pages/separatemap/search-box/inputs/inputs.tsx index edd9a469f..431767d70 100644 --- a/src/app/pages/separatemap/search-box/inputs/inputs.tsx +++ b/src/app/pages/separatemap/search-box/inputs/inputs.tsx @@ -14,18 +14,26 @@ type CommonViewProps = { toggleFilters: () => void; searchValue: string; setSearchValue: (s: string) => void; -} +}; -function FilterToggleButton({filtersHidden, toggleFilters, children}: React.PropsWithChildren< +function FilterToggleButton({ + filtersHidden, + toggleFilters, + children +}: React.PropsWithChildren< Pick >) { return ( - ); @@ -42,7 +50,10 @@ function SearchAndClear({ }) { const clearIconHiddenFlag = textValue.length < 1; - function updateOnEnter({key, target}: React.KeyboardEvent) { + function updateOnEnter({ + key, + target + }: React.KeyboardEvent) { if (key === 'Enter') { setTextValue((target as HTMLInputElement).value); } @@ -70,13 +81,19 @@ function SearchAndClear({ ); } -function SearchIcon({minimized, toggle}: Pick) { +function SearchIcon({ + minimized, + toggle +}: Pick) { const icon = minimized ? faSearch : faChevronLeft; return (
toggle()} + className="search-icon" + role="button" + aria-pressed={minimized} + tabIndex={0} + onClick={() => toggle()} aria-label="toggle search window" > @@ -85,18 +102,28 @@ function SearchIcon({minimized, toggle}: Pick) { + className, + placeholder, + children, + toggle, + minimized, + filtersHidden, + toggleFilters, + searchValue, + setSearchValue +}: React.PropsWithChildren< + { + className: string; + placeholder: string; + } & CommonViewProps +>) { return (
{children} @@ -105,7 +132,6 @@ function InputView({ ); } - export default function Inputs(commonViewProps: CommonViewProps) { return (
diff --git a/src/app/pages/separatemap/search-box/result-box/result-box.tsx b/src/app/pages/separatemap/search-box/result-box/result-box.tsx index 9dab14033..a845f06cb 100644 --- a/src/app/pages/separatemap/search-box/result-box/result-box.tsx +++ b/src/app/pages/separatemap/search-box/result-box/result-box.tsx @@ -11,28 +11,32 @@ const format = new window.Intl.NumberFormat('en-US', { currency: 'USD' }).format; -function Testimonial({testimonial}: {testimonial: Required['testimonial']}) { +function Testimonial({ + testimonial +}: { + testimonial: Required['testimonial']; +}) { return (
- + {testimonial.text}
{testimonial.name}
- { - testimonial.position && -
{testimonial.position}
- } + {testimonial.position && ( +
{testimonial.position}
+ )}
); } function SchoolDetails({model}: {model: AugmentedInfo}) { - const [ - savingsTotal, savingsThisYear, testimonial - ] = [ + const [savingsTotal, savingsThisYear, testimonial] = [ format(model.fields.all_time_savings), format(model.fields.current_year_savings), model.testimonial @@ -50,7 +54,11 @@ function SchoolDetails({model}: {model: AugmentedInfo}) { ); } -export default function ResultBox({model, theOpenOne, setTheOpenOne}: { +export default function ResultBox({ + model, + theOpenOne, + setTheOpenOne +}: { model: AugmentedInfo; theOpenOne: AugmentedInfo | null; setTheOpenOne: (m: AugmentedInfo | null) => void; @@ -71,7 +79,9 @@ export default function ResultBox({model, theOpenOne, setTheOpenOne}: { return (
@@ -79,7 +89,9 @@ export default function ResultBox({model, theOpenOne, setTheOpenOne}: {
{model.cityState}
- +
{isOpen && } diff --git a/src/app/pages/separatemap/search-box/results.tsx b/src/app/pages/separatemap/search-box/results.tsx index 57d0beb8c..4cb8a4862 100644 --- a/src/app/pages/separatemap/search-box/results.tsx +++ b/src/app/pages/separatemap/search-box/results.tsx @@ -5,15 +5,22 @@ import type {SetHandle} from '~/helpers/data'; type Results = {TOO_MANY: unknown} | AugmentedInfo[]; -function useSchoolsPromise(searchString: string, filters: SetHandle, institution: string) { +function useSchoolsPromise( + searchString: string, + filters: SetHandle, + institution: string +) { const [promise, setPromise] = useState | null>(null); useEffect(() => { const filtersArray = Array.from(filters.values()) as string[]; - const filtersDict = filtersArray.reduce((a, b) => { - a[b] = b; - return a; - }, {} as Record); + const filtersDict = filtersArray.reduce( + (a, b) => { + a[b] = b; + return a; + }, + {} as Record + ); filtersDict['institution-type'] = institution; const p = querySchools(searchString, filtersDict); @@ -27,10 +34,19 @@ function useSchoolsPromise(searchString: string, filters: SetHandle, institution } // eslint-disable-next-line complexity -export default function useResults(searchString: string, selectedFilters: SetHandle, institution: string) { - const nothingSelected = searchString === '' && +export default function useResults( + searchString: string, + selectedFilters: SetHandle, + institution: string +) { + const nothingSelected = + searchString === '' && Array.from(selectedFilters.values()).length === 0; - const schoolsPromise = useSchoolsPromise(searchString, selectedFilters, institution); + const schoolsPromise = useSchoolsPromise( + searchString, + selectedFilters, + institution + ); const results = useDataFromPromise(schoolsPromise); let message = null; diff --git a/src/app/pages/separatemap/search-box/search-box.tsx b/src/app/pages/separatemap/search-box/search-box.tsx index ffb99a0f6..128991e75 100644 --- a/src/app/pages/separatemap/search-box/search-box.tsx +++ b/src/app/pages/separatemap/search-box/search-box.tsx @@ -21,7 +21,9 @@ function useTheOpenOne({results}: {results: AugmentedInfo[]}) { useEffect(() => { if (selectedSchool) { - setTheOpenOne(results.find((r) => r.pk === selectedSchool.pk) ?? null); + setTheOpenOne( + results.find((r) => r.pk === selectedSchool.pk) ?? null + ); map.showTooltip(selectedSchool); } else { setTheOpenOne(null); @@ -32,49 +34,56 @@ function useTheOpenOne({results}: {results: AugmentedInfo[]}) { setTheOpenOne(results.length === 1 ? results[0] : null); }, [results]); - useEffect( - () => { - if (theOpenOne) { - map.showPoints([theOpenOne]); - map.showTooltip(theOpenOne); - } else if (results.length > 0) { - map.showPoints(results); - } - }, - [map, theOpenOne, results] - ); + useEffect(() => { + if (theOpenOne) { + map.showPoints([theOpenOne]); + map.showTooltip(theOpenOne); + } else if (results.length > 0) { + map.showPoints(results); + } + }, [map, theOpenOne, results]); return [theOpenOne, setTheOpenOne] as const; } -function SearchResults({minimized, results}: { +function SearchResults({ + minimized, + results +}: { minimized: boolean; results: AugmentedInfo[]; }) { const {selectedSchool} = useMapContext(); const showSelectedSchool = Boolean(results.length < 1 && selectedSchool); const resultsOrSchool = React.useMemo( - () => showSelectedSchool ? [selectedSchool as AugmentedInfo] : results, + () => + showSelectedSchool ? [selectedSchool as AugmentedInfo] : results, [showSelectedSchool, selectedSchool, results] ); const resultsHidden = resultsOrSchool.length < 1; const [theOpenOne, setTheOpenOne] = useTheOpenOne({results}); return ( -