Fixed saved Premium 93 station logic and display.
This commit is contained in:
@@ -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
|
||||
`;
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
// 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}
|
||||
/>
|
||||
|
||||
@@ -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