Community 93 Premium feature complete
This commit is contained in:
@@ -15,7 +15,9 @@ import {
|
||||
CircularProgress,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
|
||||
import { OctanePreference, SavedStation, Station, StationSearchRequest } from '../types/stations.types';
|
||||
import { CommunityStation, StationBounds } from '../types/community-stations.types';
|
||||
import {
|
||||
useStationsSearch,
|
||||
useSavedStations,
|
||||
@@ -28,9 +30,12 @@ import {
|
||||
StationsList,
|
||||
SavedStationsList,
|
||||
StationsSearchForm,
|
||||
GoogleMapsErrorBoundary
|
||||
GoogleMapsErrorBoundary,
|
||||
SubmitFor93Dialog,
|
||||
Premium93TabContent
|
||||
} from '../components';
|
||||
import { octanePreferenceToFlags, resolveSavedStationPlaceId } from '../utils/savedStations';
|
||||
import { useEnrichedStations } from '../hooks/useEnrichedStations';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -75,6 +80,8 @@ export const StationsPage: React.FC = () => {
|
||||
>();
|
||||
const [isPageReady, setIsPageReady] = useState(false);
|
||||
const [isMapReady, setIsMapReady] = useState(false);
|
||||
const [submitFor93Station, setSubmitFor93Station] = useState<Station | null>(null);
|
||||
const [searchBounds, setSearchBounds] = useState<StationBounds | null>(null);
|
||||
|
||||
// Queries and mutations
|
||||
const { mutate: search, isPending: isSearching, error: searchError } = useStationsSearch();
|
||||
@@ -87,6 +94,13 @@ export const StationsPage: React.FC = () => {
|
||||
error: savedError
|
||||
});
|
||||
|
||||
// Enrich search results with community station data
|
||||
const { enrichedStations, communityStationsMap } = useEnrichedStations(
|
||||
searchResults,
|
||||
currentLocation?.latitude ?? null,
|
||||
currentLocation?.longitude ?? null
|
||||
);
|
||||
|
||||
// Multi-stage initialization: Wait for auth, data, and DOM
|
||||
useEffect(() => {
|
||||
// Stage 1: Wait for saved stations query to settle (loading complete or error)
|
||||
@@ -124,9 +138,10 @@ export const StationsPage: React.FC = () => {
|
||||
const { mutate: updateSavedStation } = useUpdateSavedStation();
|
||||
const [octaneUpdatingId, setOctaneUpdatingId] = useState<string | null>(null);
|
||||
|
||||
// Create set of saved place IDs for quick lookup
|
||||
const { savedStationsMap, savedPlaceIds } = useMemo(() => {
|
||||
// Create set of saved place IDs and addresses for quick lookup
|
||||
const { savedStationsMap, savedPlaceIds, savedAddresses } = useMemo(() => {
|
||||
const map = new Map<string, SavedStation>();
|
||||
const addresses = new Set<string>();
|
||||
|
||||
savedStations.forEach((station) => {
|
||||
const placeId = resolveSavedStationPlaceId(station);
|
||||
@@ -138,11 +153,17 @@ export const StationsPage: React.FC = () => {
|
||||
station.placeId === placeId ? station : { ...station, placeId };
|
||||
|
||||
map.set(placeId, normalizedStation);
|
||||
|
||||
// Also track addresses for community station matching
|
||||
if (station.address) {
|
||||
addresses.add(station.address.toLowerCase().trim());
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
savedStationsMap: map,
|
||||
savedPlaceIds: new Set(map.keys())
|
||||
savedPlaceIds: new Set(map.keys()),
|
||||
savedAddresses: addresses
|
||||
};
|
||||
}, [savedStations]);
|
||||
|
||||
@@ -156,6 +177,15 @@ export const StationsPage: React.FC = () => {
|
||||
station.longitude !== undefined
|
||||
) as Station[];
|
||||
}
|
||||
if (tabValue === 2) {
|
||||
// Premium 93 tab: show saved stations with 93 octane
|
||||
return savedStations.filter(
|
||||
(station) =>
|
||||
station.has93Octane &&
|
||||
station.latitude !== undefined &&
|
||||
station.longitude !== undefined
|
||||
) as Station[];
|
||||
}
|
||||
// Results tab: show search results
|
||||
return searchResults;
|
||||
}, [tabValue, savedStations, searchResults]);
|
||||
@@ -165,6 +195,18 @@ export const StationsPage: React.FC = () => {
|
||||
setCurrentLocation({ latitude: request.latitude, longitude: request.longitude });
|
||||
setMapCenter({ lat: request.latitude, lng: request.longitude });
|
||||
|
||||
// Calculate approximate bounds from search location and radius
|
||||
const radiusKm = (request.radius || 5000) / 1000;
|
||||
const latDelta = radiusKm / 111; // ~111km per degree latitude
|
||||
const lngDelta = radiusKm / (111 * Math.cos(request.latitude * Math.PI / 180));
|
||||
|
||||
setSearchBounds({
|
||||
north: request.latitude + latDelta,
|
||||
south: request.latitude - latDelta,
|
||||
east: request.longitude + lngDelta,
|
||||
west: request.longitude - lngDelta
|
||||
});
|
||||
|
||||
search(request, {
|
||||
onSuccess: (stations) => {
|
||||
setSearchResults(stations);
|
||||
@@ -215,7 +257,7 @@ export const StationsPage: React.FC = () => {
|
||||
);
|
||||
|
||||
// Handle station selection - wrapped in useCallback to prevent infinite renders
|
||||
const handleSelectStation = useCallback((station: Station) => {
|
||||
const handleSelectStation = useCallback((station: Station | CommunityStation) => {
|
||||
setMapCenter({
|
||||
lat: station.latitude,
|
||||
lng: station.longitude
|
||||
@@ -283,17 +325,25 @@ export const StationsPage: React.FC = () => {
|
||||
>
|
||||
<Tab label={`Results (${searchResults.length})`} id="stations-tab-0" />
|
||||
<Tab label={`Saved (${savedStations.length})`} id="stations-tab-1" />
|
||||
<Tab
|
||||
label="Premium 93"
|
||||
icon={<LocalGasStationIcon />}
|
||||
iconPosition="start"
|
||||
id="stations-tab-2"
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<StationsList
|
||||
stations={searchResults}
|
||||
stations={enrichedStations}
|
||||
savedPlaceIds={savedPlaceIds}
|
||||
savedStationsMap={savedStationsMap}
|
||||
loading={isSearching}
|
||||
error={searchError ? (searchError as any).message : null}
|
||||
onSaveStation={handleSave}
|
||||
onDeleteStation={handleDelete}
|
||||
communityStationsMap={communityStationsMap}
|
||||
onSubmitFor93={(station) => setSubmitFor93Station(station)}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -306,6 +356,19 @@ export const StationsPage: React.FC = () => {
|
||||
onDeleteStation={handleDelete}
|
||||
onOctanePreferenceChange={handleOctanePreferenceChange}
|
||||
octaneUpdatingId={octaneUpdatingId}
|
||||
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<Premium93TabContent
|
||||
latitude={currentLocation?.latitude ?? null}
|
||||
longitude={currentLocation?.longitude ?? null}
|
||||
savedStations={savedStations?.filter(s => s.has93Octane) || []}
|
||||
onStationSelect={handleSelectStation}
|
||||
searchBounds={searchBounds}
|
||||
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
|
||||
savedAddresses={savedAddresses}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
@@ -386,12 +449,18 @@ export const StationsPage: React.FC = () => {
|
||||
>
|
||||
<Tab label={`Results (${searchResults.length})`} id="stations-tab-0" />
|
||||
<Tab label={`Saved (${savedStations.length})`} id="stations-tab-1" />
|
||||
<Tab
|
||||
label="Premium 93"
|
||||
icon={<LocalGasStationIcon />}
|
||||
iconPosition="start"
|
||||
id="stations-tab-2"
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto', padding: 2 }}>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<StationsList
|
||||
stations={searchResults}
|
||||
stations={enrichedStations}
|
||||
savedPlaceIds={savedPlaceIds}
|
||||
savedStationsMap={savedStationsMap}
|
||||
loading={isSearching}
|
||||
@@ -399,6 +468,8 @@ export const StationsPage: React.FC = () => {
|
||||
onSaveStation={handleSave}
|
||||
onDeleteStation={handleDelete}
|
||||
onSelectStation={handleSelectStation}
|
||||
communityStationsMap={communityStationsMap}
|
||||
onSubmitFor93={(station) => setSubmitFor93Station(station)}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -411,10 +482,38 @@ export const StationsPage: React.FC = () => {
|
||||
onDeleteStation={handleDelete}
|
||||
onOctanePreferenceChange={handleOctanePreferenceChange}
|
||||
octaneUpdatingId={octaneUpdatingId}
|
||||
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<Premium93TabContent
|
||||
latitude={currentLocation?.latitude ?? null}
|
||||
longitude={currentLocation?.longitude ?? null}
|
||||
savedStations={savedStations?.filter(s => s.has93Octane) || []}
|
||||
onStationSelect={handleSelectStation}
|
||||
searchBounds={searchBounds}
|
||||
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
|
||||
savedAddresses={savedAddresses}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<SubmitFor93Dialog
|
||||
open={!!submitFor93Station}
|
||||
onClose={() => setSubmitFor93Station(null)}
|
||||
station={submitFor93Station}
|
||||
communityStationId={
|
||||
submitFor93Station
|
||||
? // If it's a CommunityStation from Premium 93 tab, use its id directly
|
||||
('status' in submitFor93Station && submitFor93Station.status === 'approved')
|
||||
? (submitFor93Station as unknown as CommunityStation).id
|
||||
// Otherwise look up in map (for search results)
|
||||
: communityStationsMap.get(submitFor93Station.address?.toLowerCase().trim() || '')?.communityStationId
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user