Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

186719430 station selection list #22

Merged
merged 11 commits into from
Jan 26, 2024
6 changes: 1 addition & 5 deletions src/components/App.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
.body {
width: 360px;
}

.App {
padding: 12px;
box-sizing: border-box;
box-sizing: content-box;
display: flex;
flex-direction: column;
align-items: center;
Expand Down
4 changes: 2 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export const App = () => {

useEffect(() => {
initializePlugin({pluginName: kPluginName, version: kVersion, dimensions: kInitialDimensions});
adjustStationDataset(weatherStations); //change max data to "present"
createStationsDataset(weatherStations); //send weather station data to CODAP
adjustStationDataset(weatherStations as IWeatherStation[]); //change max data to "present"
Copy link
Member

Choose a reason for hiding this comment

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

Its fine for this PR but it would be good in a future PR to convert the imported json file to a .ts file and type the data in the file so that if the types change but the imported data does not it is caught at compile time.

createStationsDataset(weatherStations as IWeatherStation[]); //send weather station data to CODAP
}, []);

const handleOpenInfo = () => {
Expand Down
93 changes: 81 additions & 12 deletions src/components/location-picker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,96 @@
.location-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
height: 16px;
margin: 9px 12px 0 0;

.location-title {
font-weight: 600;
}

.selected-weather-station {
padding: 2px 2px 2px 8px;
background-color: $teal-light-25;
color: $teal-dark;
font-size: 10px;
font-weight: 500;
.station-distance {
font-style: italic;
.weather-station-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
right: 40px;
max-width: 311px;

.selected-weather-station {
padding: 2px 2px 2px 8px;
background-color: $teal-light-25;
color: $teal-dark;
font-size: 10px;
font-weight: 500;
display: flex;
flex-direction: row;
align-items: center;
max-width: 245px;

.station-distance {
font-style: italic;
margin-right: 4px;
}

.station-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 175px;
}
svg {
height: 12px;
width: 12px;
margin-left: 6px;
}
}
.location-edit-icon {
height: 12px;
width: 12px;

.station-selection-list {
height: 86px;
padding: 8px 13px;
box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.5);
background-color: #fff;
list-style-type: none;
margin-top: 0;
visibility: hidden;
max-width: 311px;

&.show {
visibility: visible;
z-index: 5;
}

.station-selection {
font-family: "Montserrat", sans-serif;
font-size: 10px;
font-weight: 500;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 2px 0;
gap: 4px;

.station-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #000;
max-width: 160px;
}
.station-distance {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: $teal-dark-75;
min-width: 120px;
}
&.selected-station {
background-color: $teal-medium;
}
}
}
}
}
Expand Down
168 changes: 130 additions & 38 deletions src/components/location-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,60 @@ import React, { useEffect, useRef, useState } from "react";
import classnames from "classnames";
import { autoComplete, geoLocSearch } from "../utils/geonameSearch";
import { useStateContext } from "../hooks/use-state";
import { IPlace } from "../types";
import { findNearestActiveStation } from "../utils/getWeatherStations";
import { IPlace, IStation } from "../types";
import { convertDistanceToStandard, findNearestActiveStations } from "../utils/getWeatherStations";
import OpenMapIcon from "../assets/images/icon-map.svg";
// import EditIcon from "../assets/images/icon-edit.svg";
import EditIcon from "../assets/images/icon-edit.svg";
import LocationIcon from "../assets/images/icon-location.svg";
import CurrentLocationIcon from "../assets/images/icon-current-location.svg";

import "./location-picker.scss";

export const LocationPicker = () => {
const {state, setState} = useStateContext();
const {units, location, weatherStation, weatherStationDistance, startDate, endDate} = state;
const [showMapButton, setShowMapButton] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [locationPossibilities, setLocationPossibilities] = useState<IPlace[]>([]);
const [showSelectionList, setShowSelectionList] = useState(false);
const [stationPossibilities, setStationPossibilities] = useState<IStation[]>([]);
const [showStationSelectionList, setShowStationSelectionList] = useState(false);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [arrowedIndex, setArrowedIndex] = useState<number>(-1);
const [distanceWidth, setDistanceWidth] = useState<number>(0);
const locationDivRef = useRef<HTMLDivElement>(null);
const locationInputEl = useRef<HTMLInputElement>(null);
const locationSelectionListEl = useRef<HTMLUListElement>(null);
const selectedLocation = state.location;
const unit = state.units;
const unitDistanceText = unit === "standard" ? "mi" : "km";
const stationDistance = state.weatherStationDistance && unit === "standard"
? Math.round((state.weatherStationDistance * 0.6 * 10) / 10)
:state.weatherStationDistance && Math.round(state.weatherStationDistance * 10) / 10;
const locationSelectionListElRef = useRef<HTMLUListElement>(null);
const stationSelectionListElRef = useRef<HTMLUListElement>(null);
const firstStationListedRef = useRef<HTMLSpanElement>(null);
const unitDistanceText = units === "standard" ? "mi" : "km";
const stationDistance = weatherStationDistance && units === "standard" ? convertDistanceToStandard(weatherStationDistance) : weatherStationDistance;

const handleOpenMap = () => {
//send request to CODAP to open map with available weather stations
};

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (event.target) {
if (stationSelectionListElRef.current && !stationSelectionListElRef.current.contains(event.target as Node)) {
setShowStationSelectionList(false);
}
if (locationSelectionListElRef.current && !locationSelectionListElRef.current.contains(event.target as Node)) {
setShowSelectionList(false);
setIsEditing(false);
}
}
}

// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

useEffect(() => {
if (locationInputEl.current?.value === "") {
setShowSelectionList(false);
// setSelectedLocation(undefined);
}
}, [locationInputEl.current?.value]);

Expand All @@ -47,19 +66,33 @@ export const LocationPicker = () => {
}, [isEditing]);

useEffect(() => {
if (selectedLocation) {
findNearestActiveStation(selectedLocation.latitude, selectedLocation.longitude, 80926000, "present")
.then(({station, distance}) => {
if (station) {
setState((draft) => {
draft.weatherStation = station;
draft.weatherStationDistance = distance;
});
const _startDate = startDate ? startDate : new Date( -5364662060); // 1/1/1750
const _endDate = endDate ? endDate : new Date(Date.now());
if (location) {
findNearestActiveStations(location.latitude, location.longitude, _startDate, _endDate)
.then((stationList: IStation[]) => {
if (stationList) {
setStationPossibilities(stationList);
(isEditing && stationList.length > 0) && setShowSelectionList(true);
}
});
setState((draft) => {
draft.weatherStation = stationList[0].station;
draft.weatherStationDistance = stationList[0].distance;
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[selectedLocation]);
},[endDate, isEditing, location, startDate]);

useEffect(() => {
if (showStationSelectionList) {
const listItems = stationSelectionListElRef.current?.children;
if (listItems && firstStationListedRef.current) {
const firstStationWidth = firstStationListedRef.current?.getBoundingClientRect().width;
setDistanceWidth(firstStationWidth ? firstStationWidth : 120);
}
}
},[showStationSelectionList]);

const getLocationList = () => {
if (locationInputEl.current) {
Expand All @@ -85,16 +118,25 @@ export const LocationPicker = () => {
setArrowedIndex(-1);
};

const stationSelected = (station: IWeatherStation | undefined) => {
setState(draft => {
draft.weatherStation = station;
});
setShowStationSelectionList(false);
setHoveredIndex(null);
setArrowedIndex(-1);
};

const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "ArrowDown" && locationPossibilities.length > 0) {
setHoveredIndex(0);
setArrowedIndex(0);
locationSelectionListEl.current?.focus();
locationSelectionListElRef.current?.focus();
}
};

const handleListKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const listItems = locationSelectionListEl.current?.children;
const listItems = locationSelectionListElRef.current?.children;
if (e.key === "Enter") {
placeNameSelected(locationPossibilities[arrowedIndex-1]);
} else
Expand Down Expand Up @@ -155,6 +197,34 @@ export const LocationPicker = () => {
}
};

const handleStationSelection = (ev: React.MouseEvent<HTMLLIElement>) => {
const target = ev.currentTarget;
if (target.dataset.ix !== undefined) {
const selectedLocIdx = parseInt(target.dataset.ix, 10);
if (selectedLocIdx >= 0) {
const selectedStation = stationPossibilities[selectedLocIdx].station;
stationSelected(selectedStation);
setState(draft => {
draft.weatherStation = selectedStation;
draft.weatherStationDistance = stationPossibilities[selectedLocIdx].distance;
});
}
setShowStationSelectionList(false);
}
};

const handleStationSelectionKeyDown = (e: React.KeyboardEvent<HTMLLIElement>, index: number) => {
if (e.key === "Enter") {
const selectedStation = stationPossibilities[index].station;
stationSelected(selectedStation);
setState(draft => {
draft.weatherStation = selectedStation;
draft.weatherStationDistance = stationPossibilities[index].distance;
});
setShowStationSelectionList(false);
}
};

const handleFindCurrentLocation = async() => {
navigator.geolocation.getCurrentPosition((position: GeolocationPosition) => {
const lat = position.coords.latitude;
Expand All @@ -178,20 +248,42 @@ export const LocationPicker = () => {
setIsEditing(true);
};

const handleOpenMap = () => {
//send request to CODAP to open map with available weather stations
};

return (
<div className="location-picker-container">
<div className="location-header">
<span className="location-title">Location</span>
{ selectedLocation && !isEditing &&
<div className="selected-weather-station">
{ state.weatherStation &&
<>
{state.weatherStationDistance &&
<span className="station-distance">({stationDistance} {unitDistanceText}) </span>}
<span className="station-name">{state.weatherStation?.name}</span>
{/* <EditIcon /> hide this for now until implemented*/}
</>
}
{ location && !isEditing &&
<div className="weather-station-wrapper">
<div className="selected-weather-station" onClick={()=>setShowStationSelectionList(true)}>
{ weatherStation &&
<>
<span className="station-distance">({stationDistance?.toFixed(1)} {unitDistanceText}) </span>
<span className="station-name"> {weatherStation?.name}</span>
<EditIcon />
</>
}
</div>
<ul ref={stationSelectionListElRef} className={classnames("station-selection-list", {"show": showStationSelectionList})}>
{stationPossibilities.map((station: IStation, idx: number) => {
if (station) {
const stationDistanceText = units === "standard" ? convertDistanceToStandard(station.distance) : station.distance;
return (
<li key={`${station}-${idx}`} data-ix={`${idx}`} value={station.station.ICAO}
className={classnames("station-selection", {"selected-station": station.station.name === state.weatherStation?.name})}
onMouseOver={()=>handleLocationHover(idx)} onClick={(e)=>handleStationSelection(e)} onKeyDown={(e)=>handleStationSelectionKeyDown(e,idx)}>
<span className="station-distance" ref={idx === 0 ? firstStationListedRef : null} style={{width: distanceWidth}}>
{stationDistanceText.toFixed(1)} {unitDistanceText} {idx === 0 && `from ${state.location?.name}`}
</span>
<span className="station-name"> {station.station.name}</span>
</li>
);
}
})}
</ul>
</div>
}
</div>
Expand All @@ -200,7 +292,7 @@ export const LocationPicker = () => {
<div ref={locationDivRef} className={classnames("location-input-wrapper", {"short" : showMapButton, "editing": isEditing})}
onClick={handleLocationInputClick}>
<LocationIcon />
{ selectedLocation && !isEditing
{ location && !isEditing
? <div>
<span className="selected-loc-intro">Stations near </span>
<span className="selected-loc-name">{state.location?.name}</span>
Expand All @@ -211,7 +303,7 @@ export const LocationPicker = () => {
</div>
{ isEditing &&
<ul
ref={locationSelectionListEl}
ref={locationSelectionListElRef}
className={classnames("location-selection-list", {"show": showSelectionList, "short" : showMapButton})}
onFocus={() => setHoveredIndex(null)}>
<li className={classnames("current-location-wrapper", {"geoname-candidate": hoveredIndex === -1})}
Expand Down
Loading
Loading