Fixed saved Premium 93 station logic and display.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user