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,251 @@
/**
* @ai-summary Desktop stations page with map and list layout
*/
import React, { useState, useMemo } from 'react';
import {
Grid,
Paper,
Tabs,
Tab,
Box,
Alert,
useMediaQuery,
useTheme
} from '@mui/material';
import { Station, StationSearchRequest } from '../types/stations.types';
import {
useStationsSearch,
useSavedStations,
useSaveStation,
useDeleteStation
} from '../hooks';
import {
StationMap,
StationsList,
SavedStationsList,
StationsSearchForm
} from '../components';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
const TabPanel: React.FC<TabPanelProps> = ({ children, value, index }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`stations-tabpanel-${index}`}
aria-labelledby={`stations-tab-${index}`}
>
{value === index && <Box sx={{ padding: 2 }}>{children}</Box>}
</div>
);
};
/**
* Desktop stations page layout
* Left: Map (60%), Right: Search form + Tabs (40%)
* Mobile: Stacks vertically
*/
export const StationsPage: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [tabValue, setTabValue] = useState(0);
const [searchResults, setSearchResults] = useState<Station[]>([]);
const [mapCenter, setMapCenter] = useState<{ lat: number; lng: number } | null>(null);
const [currentLocation, setCurrentLocation] = useState<
{ latitude: number; longitude: number } | undefined
>();
// Queries and mutations
const { mutate: search, isPending: isSearching, error: searchError } = useStationsSearch();
const { data: savedStations = [] } = useSavedStations();
const { mutate: saveStation } = useSaveStation();
const { mutate: deleteStation } = useDeleteStation();
// Create set of saved place IDs for quick lookup
const savedPlaceIds = useMemo(
() => new Set(savedStations.map((s) => s.placeId)),
[savedStations]
);
// Handle search
const handleSearch = (request: StationSearchRequest) => {
setCurrentLocation({ latitude: request.latitude, longitude: request.longitude });
setMapCenter({ lat: request.latitude, lng: request.longitude });
search(request, {
onSuccess: (stations) => {
setSearchResults(stations);
setTabValue(0); // Switch to results tab
}
});
};
// Handle save station
const handleSave = (station: Station) => {
saveStation(
{
placeId: station.placeId,
data: { isFavorite: true }
},
{
onSuccess: () => {
setSearchResults((prev) =>
prev.map((s) =>
s.placeId === station.placeId ? { ...s } : s
)
);
}
}
);
};
// Handle delete station
const handleDelete = (placeId: string) => {
deleteStation(placeId);
};
// If mobile, stack components vertically
if (isMobile) {
return (
<Box sx={{ padding: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
<Paper>
<StationsSearchForm onSearch={handleSearch} isSearching={isSearching} />
</Paper>
{searchError && (
<Alert severity="error">{(searchError as any).message || 'Search failed'}</Alert>
)}
<StationMap
stations={searchResults}
savedPlaceIds={savedPlaceIds}
currentLocation={currentLocation}
center={mapCenter || undefined}
height="300px"
/>
<Tabs
value={tabValue}
onChange={(_, newValue) => setTabValue(newValue)}
indicatorColor="primary"
textColor="primary"
aria-label="stations tabs"
>
<Tab label={`Results (${searchResults.length})`} id="stations-tab-0" />
<Tab label={`Saved (${savedStations.length})`} id="stations-tab-1" />
</Tabs>
<TabPanel value={tabValue} index={0}>
<StationsList
stations={searchResults}
savedPlaceIds={savedPlaceIds}
loading={isSearching}
error={searchError ? (searchError as any).message : null}
onSaveStation={handleSave}
onDeleteStation={handleDelete}
/>
</TabPanel>
<TabPanel value={tabValue} index={1}>
<SavedStationsList
stations={savedStations}
onSelectStation={(station) => {
setMapCenter({
lat: station.latitude,
lng: station.longitude
});
}}
onDeleteStation={handleDelete}
/>
</TabPanel>
</Box>
);
}
// Desktop layout: side-by-side
return (
<Grid container spacing={2} sx={{ padding: 2, height: 'calc(100vh - 80px)' }}>
{/* Left: Map (60%) */}
<Grid item xs={12} md={6} sx={{ display: 'flex', flexDirection: 'column' }}>
<Paper sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
<StationMap
stations={searchResults}
savedPlaceIds={savedPlaceIds}
currentLocation={currentLocation}
center={mapCenter || undefined}
height="100%"
/>
</Paper>
</Grid>
{/* Right: Search + Tabs (40%) */}
<Grid item xs={12} md={6} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Search Form */}
<Paper>
<StationsSearchForm onSearch={handleSearch} isSearching={isSearching} />
</Paper>
{/* Error Alert */}
{searchError && (
<Alert severity="error">{(searchError as any).message || 'Search failed'}</Alert>
)}
{/* Tabs */}
<Paper sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Tabs
value={tabValue}
onChange={(_, newValue) => setTabValue(newValue)}
indicatorColor="primary"
textColor="primary"
aria-label="stations tabs"
>
<Tab label={`Results (${searchResults.length})`} id="stations-tab-0" />
<Tab label={`Saved (${savedStations.length})`} id="stations-tab-1" />
</Tabs>
{/* Tab Content with overflow */}
<Box sx={{ flex: 1, overflow: 'auto' }}>
<TabPanel value={tabValue} index={0}>
<StationsList
stations={searchResults}
savedPlaceIds={savedPlaceIds}
loading={isSearching}
error={searchError ? (searchError as any).message : null}
onSaveStation={handleSave}
onDeleteStation={handleDelete}
onSelectStation={(station) => {
setMapCenter({
lat: station.latitude,
lng: station.longitude
});
}}
/>
</TabPanel>
<TabPanel value={tabValue} index={1}>
<SavedStationsList
stations={savedStations}
onSelectStation={(station) => {
setMapCenter({
lat: station.latitude,
lng: station.longitude
});
}}
onDeleteStation={handleDelete}
/>
</TabPanel>
</Box>
</Paper>
</Grid>
</Grid>
);
};
export default StationsPage;