Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/components/form-select/form-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function FormSelect({
}: {
label: string;
name: string;
selectAttributes: object;
selectAttributes?: object;
options: SelectItem[];
onValueUpdate?: (v: string) => void;
}) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/helpers/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export function useRefreshable<T>(getter: () => T) {
return React.useReducer(getter, getter());
}

export type SetHandle = ReturnType<typeof useSet>

// Each time the Set is updated, the handle is refreshed
// That way, the Set doesn't have to be rebuilt
export function useSet<T>(initialValue:T[]=[]) {
Expand Down
1 change: 1 addition & 0 deletions src/app/helpers/window-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type WindowWithSettings = typeof window & {
accountHref: string;
gatedContentEndpoint: string;
renewalEndpoint: string;
mapboxPK: string;
};
};

Expand Down
7 changes: 4 additions & 3 deletions src/app/models/query-schools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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({
Expand All @@ -34,15 +34,15 @@ 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 = {
center: options.center,
zoom: options.zoom
};

function setBounds(bounds) {
function setBounds(bounds?: LngLatBoundsLike) {
loaded.then((map) => {
if (bounds) {
map.fitBounds(bounds, {
Expand All @@ -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);
Expand All @@ -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<Pick<AugmentedInfo, 'lngLat'>>

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) {
Expand All @@ -93,13 +95,13 @@ export default function createMap(options) {
}
}

function showTooltip(schoolInfo, flyThere) {
function showTooltip(schoolInfo: AugmentedInfo) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing was using flyThere

if (!hasLngLat(schoolInfo)) {
return;
}
let html = `
<div class="put-away">
<button type="button">
<button type="button" aria-label="close tooltip">
&times;
</button>
</div>
Expand All @@ -109,14 +111,11 @@ export default function createMap(options) {
if (schoolInfo.cityState) {
html += `<br>${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(
Expand Down
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File diffs are in the first commit

Original file line number Diff line number Diff line change
@@ -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({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yikes. It wasn't a constructor.

container: id,
center: [-95.712891, 37.090240],
zoom: mapZoom,
Expand All @@ -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();
Expand All @@ -35,30 +36,34 @@ function useMap(id) {
return map;
}

function useSelectedSchool(map) {
const [selectedSchool, setSelectedSchool] = React.useState();
type Map = ReturnType<typeof createMap>;

function useSelectedSchool(map: Map) {
const [selectedSchool, setSelectedSchool] = React.useState<AugmentedInfo | null>(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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [
Copy link
Contributor Author

@RoyEJohnson RoyEJohnson Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having this defined inside the function was causing rerenders. This file is shown as changes in the first commit.

{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<React.SetStateAction<string>>}) {
return (
<FormSelect
name='institution-type'
selectAttributes={{onChange}}
label="Type of institution"
options={options}
onValueUpdate={setInstitution}
/>
);
}

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);
Expand All @@ -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<React.SetStateAction<string>>;
}) {
return (
<div className="filters">
<div className="institution-region">
Expand Down
5 changes: 4 additions & 1 deletion src/app/pages/separatemap/search-box/inputs/inputs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@
}

.search-clear {
cursor: pointer;
appearance: none;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it from a clickable icon with role="button" to a button element.

border: 0;
box-shadow: none;
padding: 0;

&[hidden] {
display: none;
Expand Down
Loading