208 lines
5.6 KiB
TypeScript
208 lines
5.6 KiB
TypeScript
/**
|
|
* @ai-summary Form for searching nearby gas stations
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
TextField,
|
|
Button,
|
|
Slider,
|
|
FormControl,
|
|
FormLabel,
|
|
Alert,
|
|
CircularProgress,
|
|
InputAdornment
|
|
} from '@mui/material';
|
|
import LocationIcon from '@mui/icons-material/LocationOn';
|
|
import MyLocationIcon from '@mui/icons-material/MyLocation';
|
|
import { StationSearchRequest, GeolocationError } from '../types/stations.types';
|
|
import { useGeolocation } from '../hooks';
|
|
|
|
interface StationsSearchFormProps {
|
|
onSearch: (request: StationSearchRequest) => void;
|
|
isSearching?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Search form with manual location input and geolocation button
|
|
* Radius slider: 1-25 miles, default 5 miles
|
|
*/
|
|
export const StationsSearchForm: React.FC<StationsSearchFormProps> = ({
|
|
onSearch,
|
|
isSearching = false
|
|
}) => {
|
|
const [latitude, setLatitude] = useState<number | ''>('');
|
|
const [longitude, setLongitude] = useState<number | ''>('');
|
|
const [radius, setRadius] = useState(5); // Miles
|
|
const [locationError, setLocationError] = useState<string | null>(null);
|
|
|
|
const {
|
|
coordinates,
|
|
isPending: isGeolocating,
|
|
error: geoError,
|
|
requestPermission,
|
|
clearError: clearGeoError
|
|
} = useGeolocation();
|
|
|
|
// Update form when geolocation succeeds
|
|
useEffect(() => {
|
|
if (coordinates) {
|
|
setLatitude(coordinates.latitude);
|
|
setLongitude(coordinates.longitude);
|
|
setLocationError(null);
|
|
}
|
|
}, [coordinates]);
|
|
|
|
// Handle geolocation errors
|
|
useEffect(() => {
|
|
if (geoError) {
|
|
if (geoError === GeolocationError.PERMISSION_DENIED) {
|
|
setLocationError('Location permission denied. Please enable it in browser settings.');
|
|
} else if (geoError === GeolocationError.TIMEOUT) {
|
|
setLocationError('Location request timed out. Try again.');
|
|
} else if (geoError === GeolocationError.POSITION_UNAVAILABLE) {
|
|
setLocationError('Location not available. Try a different device.');
|
|
} else {
|
|
setLocationError('Unable to get location. Please enter manually.');
|
|
}
|
|
}
|
|
}, [geoError]);
|
|
|
|
const handleUseCurrentLocation = () => {
|
|
clearGeoError();
|
|
requestPermission();
|
|
};
|
|
|
|
const handleSearch = () => {
|
|
if (latitude === '' || longitude === '') {
|
|
setLocationError('Please enter coordinates or use current location');
|
|
return;
|
|
}
|
|
|
|
const request: StationSearchRequest = {
|
|
latitude: typeof latitude === 'number' ? latitude : 0,
|
|
longitude: typeof longitude === 'number' ? longitude : 0,
|
|
radius: radius * 1609.34 // Convert miles to meters
|
|
};
|
|
|
|
onSearch(request);
|
|
};
|
|
|
|
const handleRadiusChange = (
|
|
_event: Event,
|
|
newValue: number | number[]
|
|
) => {
|
|
if (typeof newValue === 'number') {
|
|
setRadius(newValue);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
component="form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
handleSearch();
|
|
}}
|
|
sx={{ padding: 2, display: 'flex', flexDirection: 'column', gap: 2 }}
|
|
>
|
|
{/* Geolocation Button */}
|
|
<Button
|
|
variant="contained"
|
|
startIcon={isGeolocating ? <CircularProgress size={20} /> : <MyLocationIcon />}
|
|
onClick={handleUseCurrentLocation}
|
|
disabled={isGeolocating}
|
|
fullWidth
|
|
>
|
|
{isGeolocating ? 'Getting location...' : 'Use Current Location'}
|
|
</Button>
|
|
|
|
{/* Or Divider */}
|
|
<Box sx={{ textAlign: 'center', color: 'textSecondary' }}>or</Box>
|
|
|
|
{/* Manual Latitude Input */}
|
|
<TextField
|
|
label="Latitude"
|
|
type="number"
|
|
value={latitude}
|
|
onChange={(e) => {
|
|
const val = e.target.value;
|
|
setLatitude(val === '' ? '' : parseFloat(val));
|
|
}}
|
|
placeholder="37.7749"
|
|
inputProps={{ step: '0.0001', min: '-90', max: '90' }}
|
|
fullWidth
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<LocationIcon />
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
|
|
{/* Manual Longitude Input */}
|
|
<TextField
|
|
label="Longitude"
|
|
type="number"
|
|
value={longitude}
|
|
onChange={(e) => {
|
|
const val = e.target.value;
|
|
setLongitude(val === '' ? '' : parseFloat(val));
|
|
}}
|
|
placeholder="-122.4194"
|
|
inputProps={{ step: '0.0001', min: '-180', max: '180' }}
|
|
fullWidth
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<LocationIcon />
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
/>
|
|
|
|
{/* Radius Slider */}
|
|
<FormControl fullWidth>
|
|
<FormLabel>Search Radius: {radius} mi</FormLabel>
|
|
<Slider
|
|
value={radius}
|
|
onChange={handleRadiusChange}
|
|
min={1}
|
|
max={25}
|
|
step={0.5}
|
|
marks={[
|
|
{ value: 1, label: '1 mi' },
|
|
{ value: 5, label: '5 mi' },
|
|
{ value: 10, label: '10 mi' },
|
|
{ value: 25, label: '25 mi' }
|
|
]}
|
|
sx={{ marginTop: 2, marginBottom: 1 }}
|
|
/>
|
|
</FormControl>
|
|
|
|
{/* Error Messages */}
|
|
{locationError && (
|
|
<Alert severity="error">{locationError}</Alert>
|
|
)}
|
|
|
|
{/* Search Button */}
|
|
<Button
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={handleSearch}
|
|
disabled={isSearching || latitude === '' || longitude === ''}
|
|
sx={{
|
|
minHeight: '44px',
|
|
marginTop: 1
|
|
}}
|
|
>
|
|
{isSearching ? <CircularProgress size={24} /> : 'Search Stations'}
|
|
</Button>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default StationsSearchForm;
|