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

@@ -414,11 +414,11 @@ export class CommunityStationsRepository {
): Promise<CommunityStation | null> {
const query = `
SELECT * FROM community_stations
WHERE latitude BETWEEN $1 - $3 AND $1 + $3
AND longitude BETWEEN $2 - $3 AND $2 + $3
WHERE latitude BETWEEN ($1::NUMERIC - $3::NUMERIC) AND ($1::NUMERIC + $3::NUMERIC)
AND longitude BETWEEN ($2::NUMERIC - $3::NUMERIC) AND ($2::NUMERIC + $3::NUMERIC)
AND status = 'approved'
ORDER BY
(latitude - $1) * (latitude - $1) + (longitude - $2) * (longitude - $2) ASC
(latitude - $1::NUMERIC) * (latitude - $1::NUMERIC) + (longitude - $2::NUMERIC) * (longitude - $2::NUMERIC) ASC
LIMIT 1
`;

View File

@@ -5,21 +5,21 @@
You are a senior software engineer specializsing in NodeJS, Typescript, front end and back end development. You will be delegating tasks to the platform-agent, feature-agent, first-frontend-agent and quality-agent when appropriate.
*** ACTION ***
- You need to improve the Community sharing of the 93 Octane gas stations.
- These changes are going to focus on reducing administrative overhead and making community engagement easier.
- Improving the Gas Stations search result and default result logic.
- Make no assumptions.
- Ask clarifying questions.
- Ultrathink
*** CONTEXT ***
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
- The URL for this change is here. https://motovaultpro.com/garage/stations
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
*** CHANGES TO IMPLEMENT ***
- Plan and recommend the best solution for this change
- The submission of Community Verified 93 gas stations should be auto approved.
- The results cards for search results only have icons for "Navigate" "Premium 93" and "Favorite". Add a small font text below those icons with those labels. Don't make it so small it can't be viewed on Mobile.
- When you click the "Premium 93" icon, the card shows two check boxes. They currently say "Has 93 Octane" and "93 Octane is ethanol-free". This needs to be changed to radio buttons where you can select one or the other. The first option should read "Premium 93 with Ethanol" and the second option should be "Premium 93 w/o Ethanol"
- You need to add a third radio button to the same form that says "No longer has Premium 93". All three options are mutually exclusive.
- If two community users submit that a station no longer has Premium 93 it should be removed from the Community Premium 93 section and results.
- The "Premium 93" tab should be showing results when searches are ran. I want the icon for "Premium 93" to change color on the main "Results" page to change colors to the same color as an activated favorite if the station is confirmed by the community as a Premium 93 station.
- When a user searches gas stations the Premium 93 tab doesn't update with their saved stations.
- The "Results" tab correctly displays their saved station first
- The "Saved" tab has the correct gas station
- The "Premium 93" tab does NOT display the saved gas station.
- The gas station correctly displays under "Community Verified" section but even though it's saved it's not showing in the "Your Premium 93 Stations"
- Update the "Saved" and "Premium 93" result cards to act and look the same.
- Look at the screenshots of all three tabs after a search result. You can see the missing "Your Premium 93 Stations" and the icon for "Premium 93" under "Saved" is not highlighted.

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) => (
{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);
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>, station: SavedStation) => {
event.stopPropagation();
setNavAnchorEl(event.currentTarget);
setNavStation(station);
};
const handleCloseNavMenu = () => {
setNavAnchorEl(null);
setNavStation(null);
};
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>
// Fetch community stations using coordinates (if available)
const { data: communityStations = [] } = useApprovedNearbyStations(
latitude,
longitude,
5000 // 5km radius
);
// 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]);
// Use local map if we have coordinates and fetched data, otherwise fall back to passed prop
const effectiveCommunityStationsMap = localCommunityStationsMap.size > 0
? localCommunityStationsMap
: communityStationsMap;
// 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%' }}>
<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 }} />
</Box>
</ListItem>
</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"
<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}
/>
)}
</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>
);
})}
{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) => {
// Handle save community station (auto-sets has93Octane flag)
const handleSaveCommunityStation = useCallback(async (station: CommunityStation) => {
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));
await saveStation({
placeId: station.id, // Use community station ID as placeId
data: {
isFavorite: true,
has93Octane: station.has93Octane,
has93OctaneEthanolFree: station.has93OctaneEthanolFree
}
},
[updateSavedStation]
);
});
} 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}
/>