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> { ): Promise<CommunityStation | null> {
const query = ` const query = `
SELECT * FROM community_stations SELECT * FROM community_stations
WHERE latitude BETWEEN $1 - $3 AND $1 + $3 WHERE latitude BETWEEN ($1::NUMERIC - $3::NUMERIC) AND ($1::NUMERIC + $3::NUMERIC)
AND longitude BETWEEN $2 - $3 AND $2 + $3 AND longitude BETWEEN ($2::NUMERIC - $3::NUMERIC) AND ($2::NUMERIC + $3::NUMERIC)
AND status = 'approved' AND status = 'approved'
ORDER BY 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 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. 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 *** *** ACTION ***
- You need to improve the Community sharing of the 93 Octane gas stations. - Improving the Gas Stations search result and default result logic.
- These changes are going to focus on reducing administrative overhead and making community engagement easier.
- Make no assumptions. - Make no assumptions.
- Ask clarifying questions. - Ask clarifying questions.
- Ultrathink - Ultrathink
*** CONTEXT *** *** 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. - 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. - 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 *** *** CHANGES TO IMPLEMENT ***
- Plan and recommend the best solution for this change - When a user searches gas stations the Premium 93 tab doesn't update with their saved stations.
- The submission of Community Verified 93 gas stations should be auto approved. - The "Results" tab correctly displays their saved station first
- 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. - The "Saved" tab has the correct gas station
- 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" - The "Premium 93" tab does NOT display the saved gas station.
- 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. - 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"
- 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. - Update the "Saved" and "Premium 93" result cards to act and look the same.
- 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. - 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 * @ai-summary Tab content for premium 93 octane stations
*/ */
import React from 'react'; import React, { useMemo } from 'react';
import { import {
Box, Box,
CircularProgress, CircularProgress,
@@ -16,6 +16,18 @@ import { CommunityStation, StationBounds } from '../types/community-stations.typ
import { useApprovedNearbyStations, useApprovedStationsInBounds } from '../hooks/useCommunityStations'; import { useApprovedNearbyStations, useApprovedStationsInBounds } from '../hooks/useCommunityStations';
import { StationCard } from './StationCard'; import { StationCard } from './StationCard';
import { CommunityStationCard } from './CommunityStationCard'; 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 { interface Premium93TabContentProps {
latitude: number | null; latitude: number | null;
@@ -32,6 +44,8 @@ interface Premium93TabContentProps {
onSubmitFor93?: (station: CommunityStation) => void; onSubmitFor93?: (station: CommunityStation) => void;
/** Set of saved station addresses for quick lookup */ /** Set of saved station addresses for quick lookup */
savedAddresses?: Set<string>; 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, onUnsaveCommunityStation,
onSubmitFor93, onSubmitFor93,
savedAddresses = new Set(), 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 // Use bounds-based search when available, otherwise use nearby search
const { const {
@@ -73,8 +89,37 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
// Use bounds stations if available, otherwise nearby // Use bounds stations if available, otherwise nearby
const communityStations = searchBounds ? boundsStations : nearbyStations; const communityStations = searchBounds ? boundsStations : nearbyStations;
// Filter saved stations for 93 octane // Build local community stations map from fetched data (not from passed prop which may be empty)
const saved93Stations = savedStations.filter(s => s.has93Octane === true); 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( const nearbyApproved93Stations = (communityStations as CommunityStation[]).filter(
s => s.has93Octane === true && s.status === 'approved' s => s.has93Octane === true && s.status === 'approved'
@@ -82,7 +127,7 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Section 1: Your Saved 93 Stations */} {/* Section 1: Your Premium 93 Stations */}
<Paper <Paper
variant="outlined" variant="outlined"
sx={{ sx={{
@@ -93,7 +138,7 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
> >
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}> <Typography variant="h6" sx={{ fontWeight: 600, mb: 0.5 }}>
Your Saved 93 Stations Your Premium 93 Stations
</Typography> </Typography>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
{saved93Stations.length} station{saved93Stations.length !== 1 ? 's' : ''} saved {saved93Stations.length} station{saved93Stations.length !== 1 ? 's' : ''} saved
@@ -106,16 +151,23 @@ export const Premium93TabContent: React.FC<Premium93TabContentProps> = ({
</Alert> </Alert>
) : ( ) : (
<Grid container spacing={2}> <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}> <Grid item xs={12} sm={6} md={4} key={station.id}>
<StationCard <StationCard
station={station} station={station}
isSaved={true} isSaved={true}
savedStation={station} savedStation={station}
onSelect={onStationSelect} onSelect={onStationSelect}
communityData={communityData}
/> />
</Grid> </Grid>
))} );
})}
</Grid> </Grid>
)} )}
</Paper> </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 { import {
List,
ListItem,
ListItemButton,
ListItemText,
Box, Box,
Typography, Typography,
Chip,
Divider,
Alert, Alert,
Skeleton, Skeleton,
Menu, Grid,
MenuItem, Card,
IconButton CardContent
} from '@mui/material'; } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import { SavedStation, Station } from '../types/stations.types';
import NavigationIcon from '@mui/icons-material/Navigation'; import { CommunityStation } from '../types/community-stations.types';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; import { resolveSavedStationPlaceId } from '../utils/savedStations';
import { OctanePreference, SavedStation } from '../types/stations.types'; import { StationCard } from './StationCard';
import { formatDistance } from '../utils/distance'; import { CommunityStationData } from '../hooks/useEnrichedStations';
import { import { useApprovedNearbyStations } from '../hooks/useCommunityStations';
getOctanePreferenceFromFlags,
resolveSavedStationAddress,
resolveSavedStationName,
resolveSavedStationPlaceId
} from '../utils/savedStations';
import { OctanePreferenceSelector } from './OctanePreferenceSelector';
import { buildNavigationLinks } from '../utils/navigation-links';
interface SavedStationsListProps { interface SavedStationsListProps {
stations: SavedStation[]; stations: SavedStation[];
@@ -38,9 +25,47 @@ interface SavedStationsListProps {
error?: string | null; error?: string | null;
onSelectStation?: (station: SavedStation) => void; onSelectStation?: (station: SavedStation) => void;
onDeleteStation?: (placeId: string) => void; onDeleteStation?: (placeId: string) => void;
onOctanePreferenceChange?: (placeId: string, preference: OctanePreference) => void;
octaneUpdatingId?: string | null;
onSubmitFor93?: (station: SavedStation) => void; 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> = ({ export const SavedStationsList: React.FC<SavedStationsListProps> = ({
@@ -49,83 +74,84 @@ export const SavedStationsList: React.FC<SavedStationsListProps> = ({
error = null, error = null,
onSelectStation, onSelectStation,
onDeleteStation, onDeleteStation,
onOctanePreferenceChange, onSubmitFor93,
octaneUpdatingId, communityStationsMap = new Map(),
onSubmitFor93 latitude = null,
longitude = null
}) => { }) => {
const [navAnchorEl, setNavAnchorEl] = React.useState<null | HTMLElement>(null); // Fetch community stations using coordinates (if available)
const [navStation, setNavStation] = React.useState<SavedStation | null>(null); const { data: communityStations = [] } = useApprovedNearbyStations(
latitude,
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>, station: SavedStation) => { longitude,
event.stopPropagation(); 5000 // 5km radius
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>
); );
// 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) { if (loading) {
return ( return (
<List sx={{ width: '100%', bgcolor: 'background.paper' }}> <Grid container spacing={2}>
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<ListItem key={i} sx={{ padding: 2 }}> <Grid item xs={12} sm={6} md={4} key={i}>
<Box sx={{ width: '100%' }}> <Card>
<Skeleton variant="rectangular" height={200} />
<CardContent>
<Skeleton variant="text" width="60%" height={24} /> <Skeleton variant="text" width="60%" height={24} />
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 0.5 }} /> <Skeleton variant="text" width="80%" height={20} sx={{ mt: 0.5 }} />
<Skeleton variant="text" width="40%" height={16} sx={{ mt: 0.5 }} /> <Skeleton variant="text" width="40%" height={16} sx={{ mt: 0.5 }} />
</Box> </CardContent>
</ListItem> </Card>
</Grid>
))} ))}
</List> </Grid>
); );
} }
@@ -149,199 +175,30 @@ export const SavedStationsList: React.FC<SavedStationsListProps> = ({
} }
return ( return (
<List <Grid container spacing={2}>
sx={{ {stations.map((savedStation) => {
width: '100%', const station = savedStationToStation(savedStation);
bgcolor: 'background.paper' const placeId = station.placeId;
}}
> // Look up community data by normalized address
{stations.map((station, index) => { const normalizedAddr = normalizeAddress(savedStation.address || '');
const placeId = resolveSavedStationPlaceId(station); const communityData = effectiveCommunityStationsMap.get(normalizedAddr);
const octanePreference = getOctanePreferenceFromFlags(
station.has93Octane ?? false,
station.has93OctaneEthanolFree ?? false
);
return ( return (
<React.Fragment key={placeId ?? station.id}> <Grid item xs={12} sm={6} md={4} key={placeId}>
<ListItem <StationCard
disablePadding station={station}
sx={{ isSaved={true}
'&:hover': { savedStation={savedStation}
backgroundColor: 'action.hover' onDelete={handleDelete}
} onSelect={handleStationSelect}
}} onSubmitFor93={handleSubmitFor93}
> communityData={communityData}
<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>
</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>
); );
})} })}
{renderNavMenu()} </Grid>
</List>
); );
}; };

View File

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

View File

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

View File

@@ -16,14 +16,14 @@ import {
Typography Typography
} from '@mui/material'; } from '@mui/material';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; 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 { CommunityStation, StationBounds } from '../types/community-stations.types';
import { import {
useStationsSearch, useStationsSearch,
useSavedStations, useSavedStations,
useSaveStation, useSaveStation,
useDeleteStation, useDeleteStation,
useUpdateSavedStation useGeolocation
} from '../hooks'; } from '../hooks';
import { import {
StationMap, StationMap,
@@ -34,7 +34,7 @@ import {
SubmitFor93Dialog, SubmitFor93Dialog,
Premium93TabContent Premium93TabContent
} from '../components'; } from '../components';
import { octanePreferenceToFlags, resolveSavedStationPlaceId } from '../utils/savedStations'; import { resolveSavedStationPlaceId } from '../utils/savedStations';
import { useEnrichedStations } from '../hooks/useEnrichedStations'; import { useEnrichedStations } from '../hooks/useEnrichedStations';
interface TabPanelProps { interface TabPanelProps {
@@ -94,6 +94,13 @@ export const StationsPage: React.FC = () => {
error: savedError 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 // Enrich search results with community station data
const { enrichedStations, communityStationsMap } = useEnrichedStations( const { enrichedStations, communityStationsMap } = useEnrichedStations(
searchResults, searchResults,
@@ -135,8 +142,6 @@ export const StationsPage: React.FC = () => {
const { mutate: saveStation } = useSaveStation(); const { mutate: saveStation } = useSaveStation();
const { mutate: deleteStation } = useDeleteStation(); 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 // Create set of saved place IDs and addresses for quick lookup
const { savedStationsMap, savedPlaceIds, savedAddresses } = useMemo(() => { 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) => { 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( saveStation(
{ {
placeId: station.placeId, placeId: station.placeId,
data: { isFavorite: true } data: {
isFavorite: true,
has93Octane: communityData?.has93Octane,
has93OctaneEthanolFree: communityData?.has93OctaneEthanolFree
}
}, },
{ {
onSuccess: () => { 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 // Handle delete station
const handleDelete = (placeId: string) => { const handleDelete = (placeId: string) => {
deleteStation(placeId); 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 // Handle station selection - wrapped in useCallback to prevent infinite renders
const handleSelectStation = useCallback((station: Station | CommunityStation) => { const handleSelectStation = useCallback((station: Station | CommunityStation) => {
setMapCenter({ setMapCenter({
@@ -354,19 +371,23 @@ export const StationsPage: React.FC = () => {
error={savedError ? (savedError as any).message : null} error={savedError ? (savedError as any).message : null}
onSelectStation={handleSelectStation} onSelectStation={handleSelectStation}
onDeleteStation={handleDelete} onDeleteStation={handleDelete}
onOctanePreferenceChange={handleOctanePreferenceChange}
octaneUpdatingId={octaneUpdatingId}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)} onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
communityStationsMap={communityStationsMap}
latitude={effectiveLatitude}
longitude={effectiveLongitude}
/> />
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={2}> <TabPanel value={tabValue} index={2}>
<Premium93TabContent <Premium93TabContent
latitude={currentLocation?.latitude ?? null} latitude={effectiveLatitude}
longitude={currentLocation?.longitude ?? null} longitude={effectiveLongitude}
savedStations={savedStations?.filter(s => s.has93Octane) || []} savedStations={savedStations}
communityStationsMap={communityStationsMap}
onStationSelect={handleSelectStation} onStationSelect={handleSelectStation}
searchBounds={searchBounds} searchBounds={searchBounds}
onSaveCommunityStation={handleSaveCommunityStation}
onUnsaveCommunityStation={handleUnsaveCommunityStation}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)} onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
savedAddresses={savedAddresses} savedAddresses={savedAddresses}
/> />
@@ -480,19 +501,23 @@ export const StationsPage: React.FC = () => {
error={savedError ? (savedError as any).message : null} error={savedError ? (savedError as any).message : null}
onSelectStation={handleSelectStation} onSelectStation={handleSelectStation}
onDeleteStation={handleDelete} onDeleteStation={handleDelete}
onOctanePreferenceChange={handleOctanePreferenceChange}
octaneUpdatingId={octaneUpdatingId}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)} onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
communityStationsMap={communityStationsMap}
latitude={effectiveLatitude}
longitude={effectiveLongitude}
/> />
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={2}> <TabPanel value={tabValue} index={2}>
<Premium93TabContent <Premium93TabContent
latitude={currentLocation?.latitude ?? null} latitude={effectiveLatitude}
longitude={currentLocation?.longitude ?? null} longitude={effectiveLongitude}
savedStations={savedStations?.filter(s => s.has93Octane) || []} savedStations={savedStations}
communityStationsMap={communityStationsMap}
onStationSelect={handleSelectStation} onStationSelect={handleSelectStation}
searchBounds={searchBounds} searchBounds={searchBounds}
onSaveCommunityStation={handleSaveCommunityStation}
onUnsaveCommunityStation={handleUnsaveCommunityStation}
onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)} onSubmitFor93={(station) => setSubmitFor93Station(station as unknown as Station)}
savedAddresses={savedAddresses} savedAddresses={savedAddresses}
/> />