/** * @ai-summary Desktop stations page with map and list layout */ import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { Grid, Paper, Tabs, Tab, Box, Alert, useMediaQuery, useTheme, CircularProgress, Typography } from '@mui/material'; import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; import { SavedStation, Station, StationSearchRequest } from '../types/stations.types'; import { CommunityStation, StationBounds } from '../types/community-stations.types'; import { useStationsSearch, useSavedStations, useSaveStation, useDeleteStation, useGeolocation } from '../hooks'; import { StationMap, StationsList, SavedStationsList, StationsSearchForm, GoogleMapsErrorBoundary, SubmitFor93Dialog, Premium93TabContent } from '../components'; import { resolveSavedStationPlaceId } from '../utils/savedStations'; import { useEnrichedStations } from '../hooks/useEnrichedStations'; interface TabPanelProps { children?: React.ReactNode; index: number; value: number; } const TabPanel: React.FC = ({ children, value, index }) => { // Only render content when tab is active to prevent IntersectionObserver errors // on hidden MUI components if (value !== index) { return null; } return (
{children}
); }; /** * Desktop stations page layout * Left: Map (60%), Right: Search form + Tabs (40%) * Mobile: Stacks vertically */ export const StationsPage: React.FC = () => { console.log('[DEBUG StationsPage] Rendering'); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const [tabValue, setTabValue] = useState(0); const [searchResults, setSearchResults] = useState([]); const [mapCenter, setMapCenter] = useState<{ lat: number; lng: number } | null>(null); const [currentLocation, setCurrentLocation] = useState< { latitude: number; longitude: number } | undefined >(); const [isPageReady, setIsPageReady] = useState(false); const [isMapReady, setIsMapReady] = useState(false); const [submitFor93Station, setSubmitFor93Station] = useState(null); const [searchBounds, setSearchBounds] = useState(null); // Queries and mutations const { mutate: search, isPending: isSearching, error: searchError } = useStationsSearch(); const { data: savedStations = [], isLoading: isSavedLoading, error: savedError, isFetching, isSuccess } = useSavedStations(); console.log('[DEBUG StationsPage] useSavedStations result:', { data: savedStations, isLoading: isSavedLoading, isFetching, isSuccess, 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, currentLocation?.latitude ?? null, currentLocation?.longitude ?? null ); // Multi-stage initialization: Wait for auth, data, and DOM useEffect(() => { // Stage 1: Wait for saved stations query to settle (loading complete or error) const isDataReady = !isSavedLoading && !isFetching; if (isDataReady && !isPageReady) { console.log('[DEBUG StationsPage] Data ready, waiting for DOM...'); // Stage 2: Wait for DOM to be ready const timer = setTimeout(() => { setIsPageReady(true); console.log('[DEBUG StationsPage] Page ready'); }, 150); return () => clearTimeout(timer); } return undefined; }, [isSavedLoading, isFetching, isPageReady]); // Stage 3: Wait for page render to complete before allowing map initialization useEffect(() => { if (isPageReady && !isMapReady) { console.log('[DEBUG StationsPage] Page rendered, enabling map...'); const timer = setTimeout(() => { setIsMapReady(true); console.log('[DEBUG StationsPage] Map ready'); }, 100); return () => clearTimeout(timer); } return undefined; }, [isPageReady, isMapReady]); const { mutate: saveStation } = useSaveStation(); const { mutate: deleteStation } = useDeleteStation(); // Create set of saved place IDs and addresses for quick lookup const { savedStationsMap, savedPlaceIds, savedAddresses } = useMemo(() => { const map = new Map(); const addresses = new Set(); savedStations.forEach((station) => { const placeId = resolveSavedStationPlaceId(station); if (!placeId) { return; } const normalizedStation = station.placeId === placeId ? station : { ...station, placeId }; map.set(placeId, normalizedStation); // Also track addresses for community station matching if (station.address) { addresses.add(station.address.toLowerCase().trim()); } }); return { savedStationsMap: map, savedPlaceIds: new Set(map.keys()), savedAddresses: addresses }; }, [savedStations]); // Compute which stations to display on map based on active tab const mapStations = useMemo(() => { if (tabValue === 1) { // Saved tab: show saved stations with valid coordinates return savedStations.filter( (station) => station.latitude !== undefined && station.longitude !== undefined ) as Station[]; } if (tabValue === 2) { // Premium 93 tab: show saved stations with 93 octane return savedStations.filter( (station) => station.has93Octane && station.latitude !== undefined && station.longitude !== undefined ) as Station[]; } // Results tab: show search results return searchResults; }, [tabValue, savedStations, searchResults]); // Handle search const handleSearch = (request: StationSearchRequest) => { setCurrentLocation({ latitude: request.latitude, longitude: request.longitude }); setMapCenter({ lat: request.latitude, lng: request.longitude }); // Calculate approximate bounds from search location and radius const radiusKm = (request.radius || 5000) / 1000; const latDelta = radiusKm / 111; // ~111km per degree latitude const lngDelta = radiusKm / (111 * Math.cos(request.latitude * Math.PI / 180)); setSearchBounds({ north: request.latitude + latDelta, south: request.latitude - latDelta, east: request.longitude + lngDelta, west: request.longitude - lngDelta }); search(request, { onSuccess: (stations) => { setSearchResults(stations); setTabValue(0); // Switch to results tab } }); }; // 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, has93Octane: communityData?.has93Octane, has93OctaneEthanolFree: communityData?.has93OctaneEthanolFree } }, { onSuccess: () => { setSearchResults((prev) => prev.map((s) => s.placeId === station.placeId ? { ...s } : s ) ); } } ); }; // 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); }; // Handle station selection - wrapped in useCallback to prevent infinite renders const handleSelectStation = useCallback((station: Station | CommunityStation) => { setMapCenter({ lat: station.latitude, lng: station.longitude }); }, []); // Show comprehensive loading state until everything is ready if (!isPageReady || !isMapReady) { return ( Loading stations... ); } // If mobile, stack components vertically if (isMobile) { return ( {searchError && ( {(searchError as any).message || 'Search failed'} )} {isMapReady ? ( ) : ( )} setTabValue(newValue)} indicatorColor="primary" textColor="primary" aria-label="stations tabs" > } iconPosition="start" id="stations-tab-2" /> setSubmitFor93Station(station)} /> setSubmitFor93Station(station as unknown as Station)} communityStationsMap={communityStationsMap} latitude={effectiveLatitude} longitude={effectiveLongitude} /> setSubmitFor93Station(station as unknown as Station)} savedAddresses={savedAddresses} /> ); } // Desktop layout: top-row map + search, full-width results return ( {/* Map */} {isMapReady ? ( ) : ( )} {/* Search form */} {/* Error Alert */} {searchError && ( {(searchError as any).message || 'Search failed'} )} {/* Full-width Results */} setTabValue(newValue)} indicatorColor="primary" textColor="primary" aria-label="stations tabs" > } iconPosition="start" id="stations-tab-2" /> setSubmitFor93Station(station)} /> setSubmitFor93Station(station as unknown as Station)} communityStationsMap={communityStationsMap} latitude={effectiveLatitude} longitude={effectiveLongitude} /> setSubmitFor93Station(station as unknown as Station)} savedAddresses={savedAddresses} /> setSubmitFor93Station(null)} station={submitFor93Station} communityStationId={ submitFor93Station ? // If it's a CommunityStation from Premium 93 tab, use its id directly ('status' in submitFor93Station && submitFor93Station.status === 'approved') ? (submitFor93Station as unknown as CommunityStation).id // Otherwise look up in map (for search results) : communityStationsMap.get(submitFor93Station.address?.toLowerCase().trim() || '')?.communityStationId : undefined } /> ); }; export default StationsPage;