Gas Station Feature
This commit is contained in:
207
frontend/src/features/stations/components/StationsSearchForm.tsx
Normal file
207
frontend/src/features/stations/components/StationsSearchForm.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @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;
|
||||
Reference in New Issue
Block a user