Fixed saved Premium 93 station logic and display.

This commit is contained in:
Eric Gullickson
2025-12-21 13:56:59 -06:00
parent 95f5e89e48
commit 144f1d5bb0
7 changed files with 335 additions and 377 deletions

View File

@@ -2,7 +2,7 @@
* @ai-summary Tab content for premium 93 octane stations
*/
import React from 'react';
import React, { useMemo } from 'react';
import {
Box,
CircularProgress,
@@ -16,6 +16,18 @@ import { CommunityStation, StationBounds } from '../types/community-stations.typ
import { useApprovedNearbyStations, useApprovedStationsInBounds } from '../hooks/useCommunityStations';
import { StationCard } from './StationCard';
import { CommunityStationCard } from './CommunityStationCard';
import { CommunityStationData } from '../hooks/useEnrichedStations';
/**
* Normalize an address for comparison with community stations map
*/
function normalizeAddress(address: string): string {
return address
.toLowerCase()
.trim()
.replace(/\s+/g, ' ')
.replace(/[,]/g, '');
}
interface Premium93TabContentProps {
latitude: number | null;
@@ -32,6 +44,8 @@ interface Premium93TabContentProps {
onSubmitFor93?: (station: CommunityStation) => void;
/** Set of saved station addresses for quick lookup */
savedAddresses?: Set<string>;
/** Map of normalized address to community station data for 93 octane enrichment */
communityStationsMap?: Map<string, CommunityStationData>;
}
/**
@@ -48,6 +62,8 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
onUnsaveCommunityStation,
onSubmitFor93,
savedAddresses = new Set(),
// communityStationsMap prop is kept for backwards compatibility but we build our own local map
communityStationsMap: _communityStationsMap = new Map(),
}) => {
// Use bounds-based search when available, otherwise use nearby search
const {
@@ -73,8 +89,37 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
// Use bounds stations if available, otherwise nearby
const communityStations = searchBounds ? boundsStations : nearbyStations;
// Filter saved stations for 93 octane
const saved93Stations = savedStations.filter(s => s.has93Octane === true);
// Build local community stations map from fetched data (not from passed prop which may be empty)
const localCommunityStationsMap = useMemo(() => {
const map = new Map<string, CommunityStationData>();
(communityStations as CommunityStation[]).forEach((station) => {
if (station.status === 'approved') {
const normalizedAddr = normalizeAddress(station.address || '');
map.set(normalizedAddr, {
has93Octane: station.has93Octane,
has93OctaneEthanolFree: station.has93OctaneEthanolFree,
isVerified: true,
verifiedAt: station.reviewedAt,
communityStationId: station.id,
});
}
});
return map;
}, [communityStations]);
// Filter saved stations for 93 octane - include if has93Octane flag OR matches community-verified
const saved93Stations = useMemo(() => {
return savedStations.filter(s => {
// Include if saved metadata has 93 octane flag
if (s.has93Octane === true) {
return true;
}
// Also include if station matches a community-verified 93 octane station
const normalizedAddr = normalizeAddress(s.address || '');
const communityData = localCommunityStationsMap.get(normalizedAddr);
return communityData?.has93Octane === true;
});
}, [savedStations, localCommunityStationsMap]);
const nearbyApproved93Stations = (communityStations as CommunityStation[]).filter(
s => s.has93Octane === true && s.status === 'approved'
@@ -82,7 +127,7 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Section 1: Your Saved 93 Stations */}
{/* Section 1: Your Premium 93 Stations */}
<Paper
variant="outlined"
sx={{
@@ -93,7 +138,7 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
>
<Box sx={{ mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
Your Saved 93 Stations
Your Premium 93 Stations
</Typography>
<Typography variant="body2" color="textSecondary">
{saved93Stations.length} station{saved93Stations.length !== 1 ? 's' : ''} saved
@@ -106,16 +151,23 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
</Alert>
) : (
<Grid container spacing={2}>
{saved93Stations.map((station) => (
<Grid item xs={12} sm={6} md={4} key={station.id}>
<StationCard
station={station}
isSaved={true}
savedStation={station}
onSelect={onStationSelect}
/>
</Grid>
))}
{saved93Stations.map((station) => {
// Look up community data by normalized address using local map
const normalizedAddr = normalizeAddress(station.address || '');
const communityData = localCommunityStationsMap.get(normalizedAddr);
return (
<Grid item xs={12} sm={6} md={4} key={station.id}>
<StationCard
station={station}
isSaved={true}
savedStation={station}
onSelect={onStationSelect}
communityData={communityData}
/>
</Grid>
);
})}
</Grid>
)}
</Paper>

View File

@@ -1,36 +1,23 @@
/**
* @ai-summary List of user's saved/favorited stations with octane metadata editing
* @ai-summary Grid of user's saved/favorited stations displayed as cards
*/
import React from 'react';
import React, { useMemo } from 'react';
import {
List,
ListItem,
ListItemButton,
ListItemText,
Box,
Typography,
Chip,
Divider,
Alert,
Skeleton,
Menu,
MenuItem,
IconButton
Grid,
Card,
CardContent
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import NavigationIcon from '@mui/icons-material/Navigation';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
import { OctanePreference, SavedStation } from '../types/stations.types';
import { formatDistance } from '../utils/distance';
import {
getOctanePreferenceFromFlags,
resolveSavedStationAddress,
resolveSavedStationName,
resolveSavedStationPlaceId
} from '../utils/savedStations';
import { OctanePreferenceSelector } from './OctanePreferenceSelector';
import { buildNavigationLinks } from '../utils/navigation-links';
import { SavedStation, Station } from '../types/stations.types';
import { CommunityStation } from '../types/community-stations.types';
import { resolveSavedStationPlaceId } from '../utils/savedStations';
import { StationCard } from './StationCard';
import { CommunityStationData } from '../hooks/useEnrichedStations';
import { useApprovedNearbyStations } from '../hooks/useCommunityStations';
interface SavedStationsListProps {
stations: SavedStation[];
@@ -38,9 +25,47 @@ interface SavedStationsListProps {
error?: string | null;
onSelectStation?: (station: SavedStation) => void;
onDeleteStation?: (placeId: string) => void;
onOctanePreferenceChange?: (placeId: string, preference: OctanePreference) => void;
octaneUpdatingId?: string | null;
onSubmitFor93?: (station: SavedStation) => void;
/** Map of normalized address to community station data for 93 octane enrichment (fallback) */
communityStationsMap?: Map<string, CommunityStationData>;
/** User's current latitude for fetching community stations */
latitude?: number | null;
/** User's current longitude for fetching community stations */
longitude?: number | null;
}
/**
* Convert SavedStation to Station format for StationCard compatibility
*/
const savedStationToStation = (saved: SavedStation): Station => ({
placeId: resolveSavedStationPlaceId(saved) || saved.id,
name: saved.nickname || saved.name || 'Unknown Station',
address: saved.address || '',
formattedAddress: saved.formattedAddress,
latitude: saved.latitude ?? 0,
longitude: saved.longitude ?? 0,
rating: saved.rating ?? 0,
distance: saved.distance,
photoReference: saved.photoReference,
isSaved: true,
savedMetadata: {
nickname: saved.nickname,
notes: saved.notes,
isFavorite: saved.isFavorite,
has93Octane: saved.has93Octane,
has93OctaneEthanolFree: saved.has93OctaneEthanolFree
}
});
/**
* Normalize an address for comparison with community stations map
*/
function normalizeAddress(address: string): string {
return address
.toLowerCase()
.trim()
.replace(/\s+/g, ' ')
.replace(/[,]/g, '');
}
export const SavedStationsList: React.FC<SavedStationsListProps> = ({
@@ -49,83 +74,84 @@ export const SavedStationsList: React.FC<SavedStationsListProps> = ({
error = null,
onSelectStation,
onDeleteStation,
onOctanePreferenceChange,
octaneUpdatingId,
onSubmitFor93
onSubmitFor93,
communityStationsMap = new Map(),
latitude = null,
longitude = null
}) => {
const [navAnchorEl, setNavAnchorEl] = React.useState<null | HTMLElement>(null);
const [navStation, setNavStation] = React.useState<SavedStation | null>(null);
// Fetch community stations using coordinates (if available)
const { data: communityStations = [] } = useApprovedNearbyStations(
latitude,
longitude,
5000 // 5km radius
);
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>, station: SavedStation) => {
event.stopPropagation();
setNavAnchorEl(event.currentTarget);
setNavStation(station);
};
// Build local community stations map from fetched data
const localCommunityStationsMap = useMemo(() => {
const map = new Map<string, CommunityStationData>();
(communityStations as CommunityStation[]).forEach((station) => {
if (station.status === 'approved') {
const normalizedAddr = normalizeAddress(station.address || '');
map.set(normalizedAddr, {
has93Octane: station.has93Octane,
has93OctaneEthanolFree: station.has93OctaneEthanolFree,
isVerified: true,
verifiedAt: station.reviewedAt,
communityStationId: station.id,
});
}
});
return map;
}, [communityStations]);
const handleCloseNavMenu = () => {
setNavAnchorEl(null);
setNavStation(null);
};
// Use local map if we have coordinates and fetched data, otherwise fall back to passed prop
const effectiveCommunityStationsMap = localCommunityStationsMap.size > 0
? localCommunityStationsMap
: communityStationsMap;
const renderNavMenu = () => {
if (!navStation) {
return null;
}
const links = buildNavigationLinks(navStation);
return (
<Menu
anchorEl={navAnchorEl}
open={Boolean(navAnchorEl)}
onClose={handleCloseNavMenu}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuItem
component="a"
href={links.google}
target="_blank"
rel="noopener"
onClick={handleCloseNavMenu}
>
Navigate in Google
</MenuItem>
<MenuItem
component="a"
href={links.apple}
target="_blank"
rel="noopener"
onClick={handleCloseNavMenu}
>
Navigate in Apple Maps
</MenuItem>
<MenuItem
component="a"
href={links.waze}
target="_blank"
rel="noopener"
onClick={handleCloseNavMenu}
>
Navigate in Waze
</MenuItem>
</Menu>
// Handler for station selection - converts Station back to SavedStation
const handleStationSelect = (station: Station) => {
const savedStation = stations.find(s =>
(resolveSavedStationPlaceId(s) || s.id) === station.placeId
);
if (savedStation && onSelectStation) {
onSelectStation(savedStation);
}
};
// Handler for delete - the bookmark button will trigger this since stations are already saved
const handleDelete = (placeId: string) => {
if (onDeleteStation) {
onDeleteStation(placeId);
}
};
// Handler for submit for 93 - converts Station back to SavedStation
const handleSubmitFor93 = (station: Station) => {
const savedStation = stations.find(s =>
(resolveSavedStationPlaceId(s) || s.id) === station.placeId
);
if (savedStation && onSubmitFor93) {
onSubmitFor93(savedStation);
}
};
if (loading) {
return (
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
<Grid container spacing={2}>
{[1, 2, 3].map((i) => (
<ListItem key={i} sx={{ padding: 2 }}>
<Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="60%" height={24} />
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 0.5 }} />
<Skeleton variant="text" width="40%" height={16} sx={{ mt: 0.5 }} />
</Box>
</ListItem>
<Grid item xs={12} sm={6} md={4} key={i}>
<Card>
<Skeleton variant="rectangular" height={200} />
<CardContent>
<Skeleton variant="text" width="60%" height={24} />
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 0.5 }} />
<Skeleton variant="text" width="40%" height={16} sx={{ mt: 0.5 }} />
</CardContent>
</Card>
</Grid>
))}
</List>
</Grid>
);
}
@@ -149,199 +175,30 @@ export const SavedStationsList: React.FC<SavedStationsListProps> = ({
}
return (
<List
sx={{
width: '100%',
bgcolor: 'background.paper'
}}
>
{stations.map((station, index) => {
const placeId = resolveSavedStationPlaceId(station);
const octanePreference = getOctanePreferenceFromFlags(
station.has93Octane ?? false,
station.has93OctaneEthanolFree ?? false
);
<Grid container spacing={2}>
{stations.map((savedStation) => {
const station = savedStationToStation(savedStation);
const placeId = station.placeId;
// Look up community data by normalized address
const normalizedAddr = normalizeAddress(savedStation.address || '');
const communityData = effectiveCommunityStationsMap.get(normalizedAddr);
return (
<React.Fragment key={placeId ?? station.id}>
<ListItem
disablePadding
sx={{
'&:hover': {
backgroundColor: 'action.hover'
}
}}
>
<ListItemButton
onClick={() => onSelectStation?.(station)}
sx={{ flex: 1 }}
>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography
variant="subtitle2"
component="span"
sx={{ fontWeight: 600 }}
>
{resolveSavedStationName(station)}
</Typography>
{station.isFavorite && (
<Chip
label="Favorite"
size="small"
color="warning"
variant="filled"
/>
)}
</Box>
}
secondary={
<Box
sx={{
marginTop: 0.5,
display: 'flex',
flexDirection: 'column',
gap: 0.5
}}
>
<Typography variant="body2" color="textSecondary">
{resolveSavedStationAddress(station)}
</Typography>
{station.notes && (
<Typography
variant="body2"
color="textSecondary"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical'
}}
>
{station.notes}
</Typography>
)}
{station.distance !== undefined && (
<Typography variant="caption" color="textSecondary">
{formatDistance(station.distance)} away
</Typography>
)}
{placeId && (
<Box sx={{ mt: 1 }}>
<OctanePreferenceSelector
value={octanePreference}
onChange={(value) => onOctanePreferenceChange?.(placeId, value)}
disabled={!onOctanePreferenceChange || octaneUpdatingId === placeId}
/>
</Box>
)}
<Box
sx={{
display: 'flex',
gap: 2,
mt: 1,
flexWrap: 'wrap'
}}
>
{/* Navigate button with label - opens menu */}
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton
onClick={(e) => handleOpenNavMenu(e, station)}
title="Get directions"
sx={{
minWidth: '44px',
minHeight: '44px'
}}
>
<NavigationIcon />
</IconButton>
<Typography
variant="caption"
sx={{
fontSize: '0.65rem',
color: 'text.secondary',
mt: -0.5,
textAlign: 'center'
}}
>
Navigate
</Typography>
</Box>
{/* Premium 93 button with label */}
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton
onClick={(e) => {
e.stopPropagation();
onSubmitFor93?.(station);
}}
title="Premium 93 status"
sx={{
minWidth: '44px',
minHeight: '44px',
color: station.has93Octane ? 'warning.main' : 'inherit'
}}
>
<LocalGasStationIcon />
</IconButton>
<Typography
variant="caption"
sx={{
fontSize: '0.65rem',
color: station.has93Octane ? 'warning.main' : 'text.secondary',
mt: -0.5,
textAlign: 'center'
}}
>
Premium 93
</Typography>
</Box>
{/* Delete button with label */}
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton
onClick={(e) => {
e.stopPropagation();
if (placeId) {
onDeleteStation?.(placeId);
}
}}
disabled={!placeId}
title="Delete saved station"
sx={{
minWidth: '44px',
minHeight: '44px',
color: 'error.main'
}}
>
<DeleteIcon />
</IconButton>
<Typography
variant="caption"
sx={{
fontSize: '0.65rem',
color: 'error.main',
mt: -0.5,
textAlign: 'center'
}}
>
Delete
</Typography>
</Box>
</Box>
</Box>
}
/>
</ListItemButton>
</ListItem>
{index < stations.length - 1 && <Divider />}
</React.Fragment>
<Grid item xs={12} sm={6} md={4} key={placeId}>
<StationCard
station={station}
isSaved={true}
savedStation={savedStation}
onDelete={handleDelete}
onSelect={handleStationSelect}
onSubmitFor93={handleSubmitFor93}
communityData={communityData}
/>
</Grid>
);
})}
{renderNavMenu()}
</List>
</Grid>
);
};

View File

@@ -83,6 +83,10 @@ export const StationCard: React.FC<StationCardProps> = ({
}
: station.savedMetadata;
// Station is 93 verified if community data says so OR saved metadata has 93 octane
const is93Verified = communityData?.isVerified || savedMetadata?.has93Octane;
const has93OctaneEthanolFree = communityData?.has93OctaneEthanolFree || savedMetadata?.has93OctaneEthanolFree;
const octaneLabel = savedMetadata?.has93Octane
? savedMetadata.has93OctaneEthanolFree
? '93 Octane · Ethanol Free'
@@ -166,12 +170,12 @@ export const StationCard: React.FC<StationCardProps> = ({
/>
)}
{/* Community verified badge */}
{communityData?.isVerified && (
{/* 93 Verified badge - show for community verified OR saved stations with 93 octane */}
{is93Verified && (
<Box sx={{ marginTop: 1 }}>
<CommunityVerifiedBadge
has93Octane={communityData.has93Octane}
has93OctaneEthanolFree={communityData.has93OctaneEthanolFree}
has93Octane={communityData?.has93Octane || savedMetadata?.has93Octane || false}
has93OctaneEthanolFree={has93OctaneEthanolFree || false}
/>
</Box>
)}
@@ -216,7 +220,7 @@ export const StationCard: React.FC<StationCardProps> = ({
</Box>
{/* Premium 93 button with label - show when not verified (allows submission) */}
{showSubmitFor93Button && !communityData?.isVerified && (
{showSubmitFor93Button && !is93Verified && (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton
size="large"
@@ -244,13 +248,13 @@ export const StationCard: React.FC<StationCardProps> = ({
</Box>
)}
{/* Premium 93 verified indicator - show when verified */}
{communityData?.isVerified && (
{/* Premium 93 verified indicator - show when verified (via community or saved) */}
{is93Verified && (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<IconButton
size="large"
onClick={handleSubmitFor93}
title="Community verified Premium 93"
title="Premium 93 verified"
sx={{
minWidth: '44px',
minHeight: '44px',

View File

@@ -34,7 +34,6 @@ import {
useSavedStations,
useSaveStation,
useDeleteStation,
useUpdateSavedStation,
useGeolocation,
useEnrichedStations
} from '../hooks';
@@ -42,11 +41,10 @@ import {
import {
Station,
SavedStation,
StationSearchRequest,
OctanePreference
StationSearchRequest
} from '../types/stations.types';
import { CommunityStation, StationBounds } from '../types/community-stations.types';
import { octanePreferenceToFlags, resolveSavedStationPlaceId } from '../utils/savedStations';
import { resolveSavedStationPlaceId } from '../utils/savedStations';
import { buildNavigationLinks } from '../utils/navigation-links';
// Tab indices
@@ -67,7 +65,6 @@ export const StationsMobileScreen: React.FC = () => {
// Bottom sheet state
const [selectedStation, setSelectedStation] = useState<Station | SavedStation | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [octaneUpdatingId, setOctaneUpdatingId] = useState<string | null>(null);
const [submitFor93Station, setSubmitFor93Station] = useState<Station | null>(null);
const [searchBounds, setSearchBounds] = useState<StationBounds | null>(null);
@@ -88,7 +85,6 @@ export const StationsMobileScreen: React.FC = () => {
const { mutateAsync: saveStation } = useSaveStation();
const { mutateAsync: deleteStation } = useDeleteStation();
const { mutateAsync: updateSavedStation } = useUpdateSavedStation();
// Enrich search results with community station data
const { enrichedStations, communityStationsMap } = useEnrichedStations(
@@ -159,20 +155,30 @@ export const StationsMobileScreen: React.FC = () => {
}
}, []);
// Handle save station
// Handle save station - auto-copies 93 octane data from community verification
const handleSaveStation = useCallback(async (station: Station) => {
try {
// Get community data for this station if available
const normalizedAddress = station.address
?.toLowerCase()
.trim()
.replace(/\s+/g, ' ')
.replace(/[,]/g, '') || '';
const communityData = communityStationsMap.get(normalizedAddress);
await saveStation({
placeId: station.placeId,
data: {
nickname: station.name,
isFavorite: false
isFavorite: false,
has93Octane: communityData?.has93Octane,
has93OctaneEthanolFree: communityData?.has93OctaneEthanolFree
}
});
} catch (error) {
console.error('Failed to save station:', error);
}
}, [saveStation]);
}, [saveStation, communityStationsMap]);
// Handle delete station
const handleDeleteStation = useCallback(async (placeId: string) => {
@@ -189,20 +195,30 @@ export const StationsMobileScreen: React.FC = () => {
}
}, [deleteStation, selectedStation]);
const handleOctanePreferenceChange = useCallback(
async (placeId: string, preference: OctanePreference) => {
try {
setOctaneUpdatingId(placeId);
const data = octanePreferenceToFlags(preference);
await updateSavedStation({ placeId, data });
} catch (error) {
console.error('Failed to update octane preference:', error);
} finally {
setOctaneUpdatingId((current) => (current === placeId ? null : current));
}
},
[updateSavedStation]
);
// Handle save community station (auto-sets has93Octane flag)
const handleSaveCommunityStation = useCallback(async (station: CommunityStation) => {
try {
await saveStation({
placeId: station.id, // Use community station ID as placeId
data: {
isFavorite: true,
has93Octane: station.has93Octane,
has93OctaneEthanolFree: station.has93OctaneEthanolFree
}
});
} catch (error) {
console.error('Failed to save community station:', error);
}
}, [saveStation]);
// Handle unsave community station
const handleUnsaveCommunityStation = useCallback(async (stationId: string) => {
try {
await deleteStation(stationId);
} catch (error) {
console.error('Failed to unsave community station:', error);
}
}, [deleteStation]);
// Close bottom sheet
const handleCloseDrawer = useCallback(() => {
@@ -326,16 +342,17 @@ export const StationsMobileScreen: React.FC = () => {
{/* Saved Tab */}
{activeTab === TAB_SAVED && (
<Box sx={{ height: '100%' }}>
<Box sx={{ p: 2 }}>
<SavedStationsList
stations={savedStations || []}
loading={isLoadingSaved}
error={savedError ? 'Failed to load saved stations' : null}
onSelectStation={handleSelectStation}
onDeleteStation={handleDeleteStation}
onOctanePreferenceChange={handleOctanePreferenceChange}
octaneUpdatingId={octaneUpdatingId}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
communityStationsMap={communityStationsMap}
latitude={coordinates?.latitude ?? null}
longitude={coordinates?.longitude ?? null}
/>
</Box>
)}
@@ -346,9 +363,12 @@ export const StationsMobileScreen: React.FC = () => {
<Premium93TabContent
latitude={coordinates?.latitude ?? null}
longitude={coordinates?.longitude ?? null}
savedStations={savedStations?.filter(s => s.has93Octane) || []}
savedStations={savedStations || []}
communityStationsMap={communityStationsMap}
onStationSelect={handleSelectPremium93Station}
searchBounds={searchBounds}
onSaveCommunityStation={handleSaveCommunityStation}
onUnsaveCommunityStation={handleUnsaveCommunityStation}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
savedAddresses={savedAddresses}
/>

View File

@@ -16,14 +16,14 @@ import {
Typography
} from '@mui/material';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
import { OctanePreference, SavedStation, Station, StationSearchRequest } from '../types/stations.types';
import { SavedStation, Station, StationSearchRequest } from '../types/stations.types';
import { CommunityStation, StationBounds } from '../types/community-stations.types';
import {
useStationsSearch,
useSavedStations,
useSaveStation,
useDeleteStation,
useUpdateSavedStation
useGeolocation
} from '../hooks';
import {
StationMap,
@@ -34,7 +34,7 @@ import {
SubmitFor93Dialog,
Premium93TabContent
} from '../components';
import { octanePreferenceToFlags, resolveSavedStationPlaceId } from '../utils/savedStations';
import { resolveSavedStationPlaceId } from '../utils/savedStations';
import { useEnrichedStations } from '../hooks/useEnrichedStations';
interface TabPanelProps {
@@ -94,6 +94,13 @@ export const StationsPage: React.FC = () => {
error: savedError
});
// Get user's geolocation for community station lookups (fallback when no search performed)
const { coordinates: geoCoordinates } = useGeolocation();
// Effective coordinates: use search location if available, otherwise use geolocation
const effectiveLatitude = currentLocation?.latitude ?? geoCoordinates?.latitude ?? null;
const effectiveLongitude = currentLocation?.longitude ?? geoCoordinates?.longitude ?? null;
// Enrich search results with community station data
const { enrichedStations, communityStationsMap } = useEnrichedStations(
searchResults,
@@ -135,8 +142,6 @@ export const StationsPage: React.FC = () => {
const { mutate: saveStation } = useSaveStation();
const { mutate: deleteStation } = useDeleteStation();
const { mutate: updateSavedStation } = useUpdateSavedStation();
const [octaneUpdatingId, setOctaneUpdatingId] = useState<string | null>(null);
// Create set of saved place IDs and addresses for quick lookup
const { savedStationsMap, savedPlaceIds, savedAddresses } = useMemo(() => {
@@ -215,12 +220,24 @@ export const StationsPage: React.FC = () => {
});
};
// Handle save station
// Handle save station - auto-copies 93 octane data from community verification
const handleSave = (station: Station) => {
// Get community data for this station if available
const normalizedAddress = station.address
?.toLowerCase()
.trim()
.replace(/\s+/g, ' ')
.replace(/[,]/g, '') || '';
const communityData = communityStationsMap.get(normalizedAddress);
saveStation(
{
placeId: station.placeId,
data: { isFavorite: true }
data: {
isFavorite: true,
has93Octane: communityData?.has93Octane,
has93OctaneEthanolFree: communityData?.has93OctaneEthanolFree
}
},
{
onSuccess: () => {
@@ -234,28 +251,28 @@ export const StationsPage: React.FC = () => {
);
};
// Handle save community station (auto-sets has93Octane flag)
const handleSaveCommunityStation = useCallback((station: CommunityStation) => {
saveStation({
placeId: station.id, // Use community station ID as placeId
data: {
isFavorite: true,
has93Octane: station.has93Octane,
has93OctaneEthanolFree: station.has93OctaneEthanolFree
}
});
}, [saveStation]);
// Handle unsave community station
const handleUnsaveCommunityStation = useCallback((stationId: string) => {
deleteStation(stationId);
}, [deleteStation]);
// Handle delete station
const handleDelete = (placeId: string) => {
deleteStation(placeId);
};
const handleOctanePreferenceChange = useCallback(
(placeId: string, preference: OctanePreference) => {
const flags = octanePreferenceToFlags(preference);
setOctaneUpdatingId(placeId);
updateSavedStation(
{ placeId, data: flags },
{
onSettled: () => {
setOctaneUpdatingId((current) => (current === placeId ? null : current));
}
}
);
},
[updateSavedStation]
);
// Handle station selection - wrapped in useCallback to prevent infinite renders
const handleSelectStation = useCallback((station: Station | CommunityStation) => {
setMapCenter({
@@ -354,19 +371,23 @@ export const StationsPage: React.FC = () => {
error={savedError ? (savedError as any).message : null}
onSelectStation={handleSelectStation}
onDeleteStation={handleDelete}
onOctanePreferenceChange={handleOctanePreferenceChange}
octaneUpdatingId={octaneUpdatingId}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
communityStationsMap={communityStationsMap}
latitude={effectiveLatitude}
longitude={effectiveLongitude}
/>
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Premium93TabContent
latitude={currentLocation?.latitude ?? null}
longitude={currentLocation?.longitude ?? null}
savedStations={savedStations?.filter(s => s.has93Octane) || []}
latitude={effectiveLatitude}
longitude={effectiveLongitude}
savedStations={savedStations}
communityStationsMap={communityStationsMap}
onStationSelect={handleSelectStation}
searchBounds={searchBounds}
onSaveCommunityStation={handleSaveCommunityStation}
onUnsaveCommunityStation={handleUnsaveCommunityStation}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
savedAddresses={savedAddresses}
/>
@@ -480,19 +501,23 @@ export const StationsPage: React.FC = () => {
error={savedError ? (savedError as any).message : null}
onSelectStation={handleSelectStation}
onDeleteStation={handleDelete}
onOctanePreferenceChange={handleOctanePreferenceChange}
octaneUpdatingId={octaneUpdatingId}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
communityStationsMap={communityStationsMap}
latitude={effectiveLatitude}
longitude={effectiveLongitude}
/>
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Premium93TabContent
latitude={currentLocation?.latitude ?? null}
longitude={currentLocation?.longitude ?? null}
savedStations={savedStations?.filter(s => s.has93Octane) || []}
latitude={effectiveLatitude}
longitude={effectiveLongitude}
savedStations={savedStations}
communityStationsMap={communityStationsMap}
onStationSelect={handleSelectStation}
searchBounds={searchBounds}
onSaveCommunityStation={handleSaveCommunityStation}
onUnsaveCommunityStation={handleUnsaveCommunityStation}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
savedAddresses={savedAddresses}
/>