Gas Station Feature

This commit is contained in:
Eric Gullickson
2025-11-04 18:46:46 -06:00
parent d8d0ada83f
commit 5dc58d73b9
61 changed files with 12952 additions and 52 deletions

View 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;