Community 93 Premium feature complete

This commit is contained in:
Eric Gullickson
2025-12-21 11:31:10 -06:00
parent 1bde31247f
commit 95f5e89e48
60 changed files with 8061 additions and 350 deletions

View File

@@ -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>
);
};