mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-29 10:18:29 -04:00
160 lines
5.6 KiB
React
160 lines
5.6 KiB
React
import React, {useState} from 'react';
|
|
|
|
import {getJson, patchJson} from '../api';
|
|
import Modal from './Modal';
|
|
import PickerButton from './PickerButton';
|
|
|
|
function searchMessage(status) {
|
|
if (status === 401) return 'Please sign in again to set your location.';
|
|
return 'We couldn\'t search for places just now. Please try again.';
|
|
}
|
|
|
|
// A button that opens a dialog for saying where you garden: search for a city
|
|
// or region and pick the right one, or use the browser's location. Choosing
|
|
// saves it and reloads the page, so the season, suggestions and neighbours
|
|
// all follow from it.
|
|
//
|
|
// Searches run when asked, not as you type: our place search (Nominatim)
|
|
// asks that clients don't autocomplete.
|
|
export default function LocationPicker({label, buttonClass, searchUrl, saveUrl}) {
|
|
const [open, setOpen] = useState(false);
|
|
const [query, setQuery] = useState('');
|
|
const [places, setPlaces] = useState(null); // null: not searched yet
|
|
const [chosen, setChosen] = useState(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [message, setMessage] = useState('');
|
|
|
|
function close() {
|
|
setOpen(false);
|
|
setPlaces(null);
|
|
setChosen(null);
|
|
setMessage('');
|
|
}
|
|
|
|
async function lookup(params) {
|
|
setBusy(true);
|
|
setMessage('');
|
|
setChosen(null);
|
|
try {
|
|
const {ok, status, data} = await getJson(`${searchUrl}?${params}`);
|
|
if (!ok) {
|
|
setPlaces(null);
|
|
setMessage(searchMessage(status));
|
|
return;
|
|
}
|
|
setPlaces(data);
|
|
if (data.length === 1) setChosen(data[0]);
|
|
} catch (error) {
|
|
setMessage(searchMessage());
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
function search(event) {
|
|
event.preventDefault();
|
|
if (query.trim().length < 2) {
|
|
setMessage('Type a city or region to search for.');
|
|
return;
|
|
}
|
|
lookup(new URLSearchParams({q: query.trim()}));
|
|
}
|
|
|
|
function useMyLocation() {
|
|
if (!navigator.geolocation) {
|
|
setMessage('Your browser can\'t share its location. Search for your city instead.');
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
setMessage('');
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => lookup(new URLSearchParams({lat: position.coords.latitude, lng: position.coords.longitude})),
|
|
() => {
|
|
setBusy(false);
|
|
setMessage('We couldn\'t get your location. Search for your city instead.');
|
|
},
|
|
{timeout: 10000}
|
|
);
|
|
}
|
|
|
|
async function save() {
|
|
setBusy(true);
|
|
setMessage('');
|
|
try {
|
|
const {ok} = await patchJson(saveUrl, {location: {name: chosen.name, latitude: chosen.latitude, longitude: chosen.longitude}});
|
|
if (!ok) throw new Error('save failed');
|
|
window.location.reload();
|
|
} catch (error) {
|
|
setBusy(false);
|
|
setMessage('Something went wrong saving that. Please try again.');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<PickerButton label={label} className={buttonClass} onClick={() => setOpen(true)} />
|
|
{open && (
|
|
<Modal title="Tell us where you garden" titleId="location-picker-title" onClose={close}>
|
|
<div className="modal-body">
|
|
<p className="text-muted">
|
|
We use this to match advice to your season and latitude, and to show you what gardeners near you are
|
|
doing. Your city or region is public, like everything else you record.
|
|
</p>
|
|
<form onSubmit={search} role="search">
|
|
<label htmlFor="location-picker-query" className="form-label">City or region</label>
|
|
<div className="input-group">
|
|
<input
|
|
id="location-picker-query"
|
|
type="search"
|
|
className="form-control"
|
|
value={query}
|
|
placeholder="For example, Wellington"
|
|
autoComplete="off"
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
/>
|
|
<button type="submit" className="btn btn-success" disabled={busy}>Search</button>
|
|
</div>
|
|
</form>
|
|
<p className="my-2">
|
|
<button type="button" className="btn btn-link p-0" onClick={useMyLocation} disabled={busy}>
|
|
Use my current location
|
|
</button>
|
|
</p>
|
|
|
|
<div aria-live="polite" className="location-picker-status">
|
|
{busy && <span className="text-muted">Looking…</span>}
|
|
{message && <span className="text-danger">{message}</span>}
|
|
{places && places.length === 0 && !busy && !message && (
|
|
<span className="text-muted">We couldn't find that. Try a nearby city, or a region.</span>
|
|
)}
|
|
</div>
|
|
|
|
{places && places.length > 0 && (
|
|
<fieldset className="location-picker-results">
|
|
<legend className="visually-hidden">Places found</legend>
|
|
{places.map((place) => (
|
|
<label key={`${place.latitude},${place.longitude}`} className="location-picker-place">
|
|
<input
|
|
type="radio"
|
|
name="location-picker-place"
|
|
checked={chosen === place}
|
|
onChange={() => setChosen(place)}
|
|
/>
|
|
<span>{place.name}</span>
|
|
</label>
|
|
))}
|
|
</fieldset>
|
|
)}
|
|
</div>
|
|
<div className="modal-footer">
|
|
<button type="button" className="btn btn-secondary" onClick={close}>Cancel</button>
|
|
<button type="button" className="btn btn-success" onClick={save} disabled={!chosen || busy}>
|
|
Save location
|
|
</button>
|
|
</div>
|
|
</Modal>
|
|
)}
|
|
</>
|
|
);
|
|
}
|