Fixed saved Premium 93 station logic and display.
This commit is contained in:
@@ -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
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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) => {
|
||||||
<Grid item xs={12} sm={6} md={4} key={station.id}>
|
// Look up community data by normalized address using local map
|
||||||
<StationCard
|
const normalizedAddr = normalizeAddress(station.address || '');
|
||||||
station={station}
|
const communityData = localCommunityStationsMap.get(normalizedAddr);
|
||||||
isSaved={true}
|
|
||||||
savedStation={station}
|
return (
|
||||||
onSelect={onStationSelect}
|
<Grid item xs={12} sm={6} md={4} key={station.id}>
|
||||||
/>
|
<StationCard
|
||||||
</Grid>
|
station={station}
|
||||||
))}
|
isSaved={true}
|
||||||
|
savedStation={station}
|
||||||
|
onSelect={onStationSelect}
|
||||||
|
communityData={communityData}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</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 {
|
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,
|
||||||
|
longitude,
|
||||||
|
5000 // 5km radius
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>, station: SavedStation) => {
|
// Build local community stations map from fetched data
|
||||||
event.stopPropagation();
|
const localCommunityStationsMap = useMemo(() => {
|
||||||
setNavAnchorEl(event.currentTarget);
|
const map = new Map<string, CommunityStationData>();
|
||||||
setNavStation(station);
|
(communityStations as CommunityStation[]).forEach((station) => {
|
||||||
};
|
if (station.status === 'approved') {
|
||||||
|
const normalizedAddr = normalizeAddress(station.address || '');
|
||||||
|
map.set(normalizedAddr, {
|
||||||
|
has93Octane: station.has93Octane,
|
||||||
|
has93OctaneEthanolFree: station.has93OctaneEthanolFree,
|
||||||
|
isVerified: true,
|
||||||
|
verifiedAt: station.reviewedAt,
|
||||||
|
communityStationId: station.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [communityStations]);
|
||||||
|
|
||||||
const handleCloseNavMenu = () => {
|
// Use local map if we have coordinates and fetched data, otherwise fall back to passed prop
|
||||||
setNavAnchorEl(null);
|
const effectiveCommunityStationsMap = localCommunityStationsMap.size > 0
|
||||||
setNavStation(null);
|
? localCommunityStationsMap
|
||||||
};
|
: communityStationsMap;
|
||||||
|
|
||||||
const renderNavMenu = () => {
|
// Handler for station selection - converts Station back to SavedStation
|
||||||
if (!navStation) {
|
const handleStationSelect = (station: Station) => {
|
||||||
return null;
|
const savedStation = stations.find(s =>
|
||||||
}
|
(resolveSavedStationPlaceId(s) || s.id) === station.placeId
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
|
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="text" width="60%" height={24} />
|
<Skeleton variant="rectangular" height={200} />
|
||||||
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 0.5 }} />
|
<CardContent>
|
||||||
<Skeleton variant="text" width="40%" height={16} sx={{ mt: 0.5 }} />
|
<Skeleton variant="text" width="60%" height={24} />
|
||||||
</Box>
|
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 0.5 }} />
|
||||||
</ListItem>
|
<Skeleton variant="text" width="40%" height={16} sx={{ mt: 0.5 }} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</List>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,199 +175,30 @@ export const SavedStationsList: React.FC<SavedStationsListProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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)}
|
</Grid>
|
||||||
sx={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
component="span"
|
|
||||||
sx={{ fontWeight: 600 }}
|
|
||||||
>
|
|
||||||
{resolveSavedStationName(station)}
|
|
||||||
</Typography>
|
|
||||||
{station.isFavorite && (
|
|
||||||
<Chip
|
|
||||||
label="Favorite"
|
|
||||||
size="small"
|
|
||||||
color="warning"
|
|
||||||
variant="filled"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
marginTop: 0.5,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 0.5
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
{resolveSavedStationAddress(station)}
|
|
||||||
</Typography>
|
|
||||||
{station.notes && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="textSecondary"
|
|
||||||
sx={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
WebkitBoxOrient: 'vertical'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{station.notes}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{station.distance !== undefined && (
|
|
||||||
<Typography variant="caption" color="textSecondary">
|
|
||||||
{formatDistance(station.distance)} away
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{placeId && (
|
|
||||||
<Box sx={{ mt: 1 }}>
|
|
||||||
<OctanePreferenceSelector
|
|
||||||
value={octanePreference}
|
|
||||||
onChange={(value) => onOctanePreferenceChange?.(placeId, value)}
|
|
||||||
disabled={!onOctanePreferenceChange || octaneUpdatingId === placeId}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 2,
|
|
||||||
mt: 1,
|
|
||||||
flexWrap: 'wrap'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Navigate button with label - opens menu */}
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
||||||
<IconButton
|
|
||||||
onClick={(e) => handleOpenNavMenu(e, station)}
|
|
||||||
title="Get directions"
|
|
||||||
sx={{
|
|
||||||
minWidth: '44px',
|
|
||||||
minHeight: '44px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<NavigationIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.65rem',
|
|
||||||
color: 'text.secondary',
|
|
||||||
mt: -0.5,
|
|
||||||
textAlign: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Navigate
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Premium 93 button with label */}
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
||||||
<IconButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSubmitFor93?.(station);
|
|
||||||
}}
|
|
||||||
title="Premium 93 status"
|
|
||||||
sx={{
|
|
||||||
minWidth: '44px',
|
|
||||||
minHeight: '44px',
|
|
||||||
color: station.has93Octane ? 'warning.main' : 'inherit'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LocalGasStationIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.65rem',
|
|
||||||
color: station.has93Octane ? 'warning.main' : 'text.secondary',
|
|
||||||
mt: -0.5,
|
|
||||||
textAlign: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Premium 93
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Delete button with label */}
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
||||||
<IconButton
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (placeId) {
|
|
||||||
onDeleteStation?.(placeId);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!placeId}
|
|
||||||
title="Delete saved station"
|
|
||||||
sx={{
|
|
||||||
minWidth: '44px',
|
|
||||||
minHeight: '44px',
|
|
||||||
color: 'error.main'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
fontSize: '0.65rem',
|
|
||||||
color: 'error.main',
|
|
||||||
mt: -0.5,
|
|
||||||
textAlign: 'center'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
{index < stations.length - 1 && <Divider />}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{renderNavMenu()}
|
</Grid>
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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));
|
}
|
||||||
}
|
});
|
||||||
},
|
} catch (error) {
|
||||||
[updateSavedStation]
|
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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user