# Gas Stations Feature - Frontend Documentation

## Overview

Complete frontend implementation for the Gas Stations feature. This feature enables users to search for nearby gas stations, view them on an interactive map, save favorites with custom notes, and integrate station data into fuel logging workflows.

## Feature Capabilities

- Search nearby gas stations using geolocation or manual coordinates
- View stations on interactive Google Maps
- Display station cards with name, address, distance, rating
- Save favorite stations with custom nicknames and notes
- Mobile-first responsive design with tab navigation
- Desktop layout with side-by-side map and list
- Integration with fuel logs (StationPicker component)

## Architecture

### Directory Structure

```
frontend/src/features/stations/
├── types/                           # TypeScript definitions
│   └── stations.types.ts            # API types, domain models
├── api/                             # API client
│   └── stations.api.ts              # HTTP calls to backend
├── hooks/                           # React Query hooks
│   ├── useStationsSearch.ts         # Search mutation
│   ├── useSavedStations.ts          # Get saved stations query
│   ├── useSaveStation.ts            # Save mutation
│   ├── useDeleteStation.ts          # Delete mutation
│   └── useGeolocation.ts            # Browser geolocation
├── utils/                           # Utility functions
│   ├── distance.ts                  # Distance calculations
│   ├── maps-loader.ts               # Lazy-load Google Maps API
│   └── map-utils.ts                 # Map helpers
├── components/                      # React components
│   ├── StationCard.tsx              # Individual station display
│   ├── StationsList.tsx             # List of search results
│   ├── SavedStationsList.tsx        # Saved stations list
│   ├── StationsSearchForm.tsx       # Search input form
│   ├── StationMap.tsx               # Interactive Google Map
│   └── index.ts                     # Component exports
├── pages/                           # Page layouts
│   └── StationsPage.tsx             # Desktop layout
├── mobile/                          # Mobile layouts
│   └── StationsMobileScreen.tsx    # Mobile tab navigation
└── README.md                        # This file
```

### Component Hierarchy

```
StationsPage (Desktop)
├── StationsSearchForm
│   └── useGeolocation
│   └── useStationsSearch
├── StationMap
│   └── Google Maps API
│   └── Station markers
└── StationsList
    └── StationCard (multiple)
        └── useSaveStation
        └── useDeleteStation

StationsMobileScreen (Mobile)
├── Tab: Search
│   ├── StationsSearchForm
│   └── StationsList
├── Tab: Saved
│   └── SavedStationsList
│       └── StationCard (multiple)
└── Tab: Map
    └── StationMap
```

## Key Components

### StationsSearchForm

**Purpose**: Search input with geolocation or manual street-level address entry

**Props**: None (uses hooks internally)

**Features**:
- Geolocation button (requests browser permission)
- Manual Street / City / State / ZIP inputs with Google geocoding
- Radius slider (1-25 miles)
- Loading states
- Error handling

**Usage**:
```tsx
import { StationsSearchForm } from '@/features/stations/components';

function MyPage() {
  return <StationsSearchForm />;
}
```

**Hooks Used**:
- `useGeolocation()` - Browser geolocation API
- `useStationsSearch()` - Search mutation

**State Management**:
- Form state via React hooks
- Search results via React Query cache

### StationCard

**Purpose**: Display individual station with actions

**Props**:
```typescript
interface StationCardProps {
  station: Station;
  distance?: number;
  isSaved?: boolean;
  onSave?: () => void;
  onDelete?: () => void;
}
```

**Features**:
- Station name, address, rating display
- Distance badge (if provided)
- Save/unsave button (heart icon)
- Directions link to Google Maps
- Touch-friendly 44px button heights
- Responsive layout

**Usage**:
```tsx
import { StationCard } from '@/features/stations/components';

function MyComponent() {
  const { data: stations } = useStationsSearch();

  return (
    <div>
      {stations?.map(station => (
        <StationCard
          key={station.placeId}
          station={station}
          distance={station.distance}
        />
      ))}
    </div>
  );
}
```

### StationMap

**Purpose**: Interactive Google Map with station markers

**Props**:
```typescript
interface StationMapProps {
  stations: Station[];
  center?: { lat: number; lng: number };
  zoom?: number;
  onStationClick?: (station: Station) => void;
}
```

**Features**:
- Lazy-load Google Maps API (via maps-loader.ts)
- Auto-fit bounds to show all stations
- Custom markers for gas stations
- Click handler for station selection
- Loading fallback UI
- Error handling (API key issues)

**Usage**:
```tsx
import { StationMap } from '@/features/stations/components';

function MyPage() {
  const { data: stations } = useStationsSearch();

  return (
    <StationMap
      stations={stations || []}
      onStationClick={(station) => {
        console.log('Selected:', station);
      }}
    />
  );
}
```

**Google Maps API Loading**:
The map component uses `maps-loader.ts` to lazy-load the Google Maps JavaScript API:
```typescript
import { loadGoogleMaps } from '@/features/stations/utils/maps-loader';

useEffect(() => {
  loadGoogleMaps()
    .then(() => {
      // Initialize map
    })
    .catch((error) => {
      // Handle error
    });
}, []);
```

### StationsList

**Purpose**: Scrollable list of station cards

**Props**:
```typescript
interface StationsListProps {
  stations: Station[];
  isLoading?: boolean;
  onStationSelect?: (station: Station) => void;
}
```

**Features**:
- Virtualized list for performance (if many results)
- Empty state messaging
- Loading skeletons
- Responsive grid layout

**Usage**:
```tsx
import { StationsList } from '@/features/stations/components';

function MyComponent() {
  const { data: stations, isLoading } = useStationsSearch();

  return (
    <StationsList
      stations={stations || []}
      isLoading={isLoading}
    />
  );
}
```

### SavedStationsList

**Purpose**: List of user's saved favorite stations

**Props**: None (uses hooks internally)

**Features**:
- Fetches saved stations on mount
- Edit nickname/notes inline
- Delete confirmation
- Empty state for no saved stations
- Pull-to-refresh (mobile)

**Usage**:
```tsx
import { SavedStationsList } from '@/features/stations/components';

function SavedTab() {
  return <SavedStationsList />;
}
```

**Hooks Used**:
- `useSavedStations()` - Fetch saved stations
- `useDeleteStation()` - Remove saved station

## React Query Hooks

### useStationsSearch

**Purpose**: Search for nearby gas stations

**Type**: Mutation (not cached, each search is independent)

**Parameters**:
```typescript
interface StationSearchRequest {
  latitude: number;
  longitude: number;
  radius?: number;  // In meters, default 5000
  fuelType?: string;
}
```

**Returns**:
```typescript
{
  mutate: (request: StationSearchRequest) => void;
  isPending: boolean;
  isError: boolean;
  error: ApiError | null;
  data: Station[] | undefined;
}
```

**Usage**:
```tsx
const { mutate: search, isPending, data, error } = useStationsSearch({
  onSuccess: (stations) => {
    console.log('Found stations:', stations);
  },
  onError: (error) => {
    console.error('Search failed:', error.message);
  }
});

const handleSearch = () => {
  search({
    latitude: 37.7749,
    longitude: -122.4194,
    radius: 5000
  });
};
```

### useSavedStations

**Purpose**: Fetch all saved stations for current user

**Type**: Query (cached with 5-minute stale time)

**Parameters**: None (user identified by JWT)

**Returns**:
```typescript
{
  data: SavedStation[] | undefined;
  isLoading: boolean;
  isError: boolean;
  error: ApiError | null;
  refetch: () => void;
}
```

**Usage**:
```tsx
const { data: savedStations, isLoading, refetch } = useSavedStations();

useEffect(() => {
  if (savedStations) {
    console.log('User has', savedStations.length, 'saved stations');
  }
}, [savedStations]);
```

### useSaveStation

**Purpose**: Save a station to favorites

**Type**: Mutation (invalidates saved stations cache)

**Parameters**:
```typescript
interface SaveStationRequest {
  placeId: string;
  nickname?: string;
  notes?: string;
  isFavorite?: boolean;
}
```

**Returns**:
```typescript
{
  mutate: (request: SaveStationRequest) => void;
  isPending: boolean;
  isError: boolean;
  error: ApiError | null;
}
```

**Usage**:
```tsx
const { mutate: saveStation, isPending } = useSaveStation({
  onSuccess: () => {
    toast.success('Station saved!');
  },
  onError: (error) => {
    toast.error(`Failed to save: ${error.message}`);
  }
});

const handleSave = (station: Station) => {
  saveStation({
    placeId: station.placeId,
    nickname: 'My Favorite Station',
    isFavorite: true
  });
};
```

### useDeleteStation

**Purpose**: Remove saved station from favorites

**Type**: Mutation (invalidates saved stations cache)

**Parameters**: `placeId: string`

**Returns**:
```typescript
{
  mutate: (placeId: string) => void;
  isPending: boolean;
  isError: boolean;
  error: ApiError | null;
}
```

**Usage**:
```tsx
const { mutate: deleteStation, isPending } = useDeleteStation({
  onSuccess: () => {
    toast.success('Station removed');
  }
});

const handleDelete = (placeId: string) => {
  if (confirm('Remove this station?')) {
    deleteStation(placeId);
  }
};
```

### useGeolocation

**Purpose**: Access browser geolocation API

**Type**: Hook (not React Query, custom hook)

**Parameters**: None

**Returns**:
```typescript
{
  location: { latitude: number; longitude: number } | null;
  error: string | null;
  isLoading: boolean;
  requestLocation: () => void;
}
```

**Usage**:
```tsx
const { location, error, isLoading, requestLocation } = useGeolocation();

const handleUseCurrentLocation = () => {
  requestLocation();
};

useEffect(() => {
  if (location) {
    console.log('User location:', location);
    // Trigger search with location
  }
}, [location]);

if (error) {
  return <div>Geolocation error: {error}</div>;
}
```

**Browser Permissions**:
- Requests `navigator.geolocation.getCurrentPosition`
- User must grant permission in browser
- Fallback to manual coordinates if denied

## Utility Functions

### distance.ts

**Purpose**: Calculate distance between two coordinates

**Functions**:
```typescript
/**
 * Calculate distance using Haversine formula
 * @param lat1 First point latitude
 * @param lon1 First point longitude
 * @param lat2 Second point latitude
 * @param lon2 Second point longitude
 * @returns Distance in meters
 */
export function calculateDistance(
  lat1: number,
  lon1: number,
  lat2: number,
  lon2: number
): number;

/**
 * Format distance for display
 * @param meters Distance in meters
 * @returns Formatted string (e.g., "1.5 km", "350 m")
 */
export function formatDistance(meters: number): string;
```

**Usage**:
```tsx
import { calculateDistance, formatDistance } from '@/features/stations/utils/distance';

const distanceInMeters = calculateDistance(
  37.7749, -122.4194,  // San Francisco
  37.7849, -122.4094   // Station location
);

const formatted = formatDistance(distanceInMeters); // "1.2 km"
```

### maps-loader.ts

**Purpose**: Lazy-load Google Maps JavaScript API

**Functions**:
```typescript
/**
 * Load Google Maps API dynamically
 * Reads API key from runtime config
 * Only loads once (cached promise)
 */
export function loadGoogleMaps(): Promise<void>;

/**
 * Check if Google Maps API is already loaded
 */
export function isGoogleMapsLoaded(): boolean;
```

**Usage**:
```tsx
import { loadGoogleMaps } from '@/features/stations/utils/maps-loader';

useEffect(() => {
  loadGoogleMaps()
    .then(() => {
      // google.maps is now available
      const map = new google.maps.Map(mapRef.current, {
        center: { lat: 37.7749, lng: -122.4194 },
        zoom: 12
      });
    })
    .catch((error) => {
      console.error('Failed to load Google Maps:', error);
    });
}, []);
```

**Runtime Configuration**:
The loader uses the runtime config pattern to access the API key:
```typescript
import { getGoogleMapsApiKey } from '@/core/config/config.types';

const apiKey = getGoogleMapsApiKey();
// Loads script: https://maps.googleapis.com/maps/api/js?key=...&libraries=places
```

### map-utils.ts

**Purpose**: Helper functions for map operations

**Functions**:
```typescript
/**
 * Fit map bounds to show all stations
 */
export function fitBoundsToStations(
  map: google.maps.Map,
  stations: Station[]
): void;

/**
 * Create marker for station
 */
export function createStationMarker(
  map: google.maps.Map,
  station: Station,
  onClick?: (station: Station) => void
): google.maps.Marker;
```

## Runtime Configuration

The Gas Stations feature uses MotoVaultPro's K8s-aligned runtime configuration pattern for the Google Maps API key.

### How It Works

1. **Container Startup**: `/app/load-config.sh` reads `/run/secrets/google-maps-api-key`
2. **Config Generation**: Creates `/usr/share/nginx/html/config.js`
3. **App Access**: React app reads `window.CONFIG.googleMapsApiKey`

### Accessing Configuration

```typescript
import { getGoogleMapsApiKey, getGoogleMapsMapId } from '@/core/config/config.types';

export function MyComponent() {
  const apiKey = getGoogleMapsApiKey();
  const mapId = getGoogleMapsMapId();

  if (!apiKey || !mapId) {
    return <div>Google Maps configuration not complete</div>;
  }

  // Use API key + map id
  return <StationMap apiKey={apiKey} mapId={mapId} />;
}
```

### Development Setup

For local development (Vite dev server):

```bash
# Set up secrets
mkdir -p ./secrets/app
echo "YOUR_API_KEY" > ./secrets/app/google-maps-api-key.txt
echo "YOUR_MAP_ID" > ./secrets/app/google-maps-map-id.txt

# Alternatively, set environment variable
export VITE_GOOGLE_MAPS_API_KEY=YOUR_API_KEY
export VITE_GOOGLE_MAPS_MAP_ID=YOUR_MAP_ID
```

See `/frontend/docs/RUNTIME-CONFIG.md` for complete documentation.

## Adding New Functionality

### Adding a New Component

1. Create component in `components/` directory
2. Follow naming convention: `ComponentName.tsx`
3. Export from `components/index.ts`
4. Add types to `types/stations.types.ts` if needed
5. Write unit tests in `__tests__/` directory

**Example**:
```tsx
// components/StationRating.tsx
import { Station } from '../types/stations.types';

interface StationRatingProps {
  station: Station;
}

export function StationRating({ station }: StationRatingProps) {
  if (!station.rating) return null;

  return (
    <div className="flex items-center gap-1">
      <span className="text-yellow-500">★</span>
      <span>{station.rating.toFixed(1)}</span>
    </div>
  );
}

// components/index.ts
export { StationRating } from './StationRating';
```

### Adding a New Hook

1. Create hook in `hooks/` directory
2. Use React Query for server state
3. Follow naming convention: `useFeatureName.ts`
4. Document with JSDoc comments and usage examples

**Example**:
```tsx
// hooks/useStationDetails.ts
import { useQuery } from '@tanstack/react-query';
import { stationsApi } from '../api/stations.api';

/**
 * Fetch detailed information for a specific station
 * Cached for 10 minutes
 */
export function useStationDetails(placeId: string) {
  return useQuery({
    queryKey: ['station', placeId],
    queryFn: () => stationsApi.getStationDetails(placeId),
    staleTime: 10 * 60 * 1000, // 10 minutes
    enabled: !!placeId
  });
}
```

### Adding a New API Endpoint

1. Add method to `api/stations.api.ts`
2. Add types to `types/stations.types.ts`
3. Create hook in `hooks/` directory
4. Update documentation

**Example**:
```typescript
// api/stations.api.ts
export const stationsApi = {
  // ... existing methods

  async getNearbyPrices(placeId: string): Promise<FuelPrices> {
    const response = await apiClient.get(`/api/stations/${placeId}/prices`);
    return response.data;
  }
};

// types/stations.types.ts
export interface FuelPrices {
  regular: number;
  premium: number;
  diesel: number;
  lastUpdated: string;
}

// hooks/useStationPrices.ts
export function useStationPrices(placeId: string) {
  return useQuery({
    queryKey: ['station-prices', placeId],
    queryFn: () => stationsApi.getNearbyPrices(placeId),
    staleTime: 5 * 60 * 1000 // 5 minutes
  });
}
```

## Testing Components

### Unit Testing

**Location**: `__tests__/components/`

**Tools**: Vitest + React Testing Library

**Example**:
```tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { StationCard } from '../components/StationCard';
import { mockStations } from './fixtures';

describe('StationCard', () => {
  it('renders station information', () => {
    render(<StationCard station={mockStations[0]} />);

    expect(screen.getByText('Shell Gas Station')).toBeInTheDocument();
    expect(screen.getByText(/123 Main St/)).toBeInTheDocument();
  });

  it('calls onSave when save button clicked', () => {
    const onSave = vi.fn();
    render(<StationCard station={mockStations[0]} onSave={onSave} />);

    fireEvent.click(screen.getByRole('button', { name: /save/i }));

    expect(onSave).toHaveBeenCalledTimes(1);
  });
});
```

### Integration Testing

Test complete workflows:

```tsx
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StationsPage } from '../pages/StationsPage';

describe('StationsPage Integration', () => {
  it('searches and displays stations', async () => {
    const queryClient = new QueryClient();

    render(
      <QueryClientProvider client={queryClient}>
        <StationsPage />
      </QueryClientProvider>
    );

    // Fill search form
    const latInput = screen.getByLabelText(/latitude/i);
    fireEvent.change(latInput, { target: { value: '37.7749' } });

    // Submit search
    const searchButton = screen.getByRole('button', { name: /search/i });
    fireEvent.click(searchButton);

    // Wait for results
    await waitFor(() => {
      expect(screen.getByText(/Shell Gas Station/i)).toBeInTheDocument();
    });
  });
});
```

## Performance Considerations

### Lazy Loading

- Google Maps API loaded only when map component mounts
- Station images lazy-loaded with Intersection Observer
- Route-based code splitting for page components

### Caching Strategy

- Search results not cached (each search is independent)
- Saved stations cached for 5 minutes
- Google Maps API script cached by browser

### Optimization Tips

1. **Limit Search Radius**: Default 5km, max 50km
2. **Pagination**: Load first 20 results, paginate if needed
3. **Virtual Scrolling**: For large result sets (100+ stations)
4. **Debounce Search**: Wait 500ms after user stops typing
5. **Memoize Calculations**: Use `useMemo` for distance calculations

**Example**:
```tsx
const sortedStations = useMemo(() => {
  if (!stations || !userLocation) return stations;

  return stations
    .map(station => ({
      ...station,
      distance: calculateDistance(
        userLocation.latitude,
        userLocation.longitude,
        station.latitude,
        station.longitude
      )
    }))
    .sort((a, b) => a.distance - b.distance);
}, [stations, userLocation]);
```

## Mobile Responsiveness

### Design Principles

- **Mobile-first**: Design for mobile, enhance for desktop
- **Touch targets**: Minimum 44px height for buttons
- **Readable text**: Minimum 16px font size (no zoom on iOS)
- **Accessible contrast**: WCAG AA compliance

### Responsive Breakpoints

```css
/* Mobile (default) */
@media (min-width: 640px) {  /* sm */
  /* Small tablets */
}

@media (min-width: 768px) {  /* md */
  /* Tablets */
}

@media (min-width: 1024px) { /* lg */
  /* Desktop - switch to side-by-side layout */
}
```

### Layout Differences

**Mobile** (< 1024px):
- Bottom tab navigation (Search, Saved, Map)
- Full-width components
- Stack vertically

**Desktop** (>= 1024px):
- Side-by-side: Map on left, List on right
- Fixed positions with scroll
- Larger interactive areas

## Troubleshooting

### Google Maps Not Loading

**Symptom**: Map shows blank or error message

**Solutions**:
1. Check API key in config:
   ```bash
   docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js
   ```
2. Verify API key in Google Cloud Console
3. Check browser console for errors
4. Verify Maps JavaScript API is enabled

### Geolocation Not Working

**Symptom**: "Use Current Location" button doesn't work

**Solutions**:
1. Check browser permissions (user must allow)
2. Requires HTTPS in production (use https://motovaultpro.com)
3. Some browsers block geolocation in iframes
4. Fallback to manual coordinates

### Stations Not Saving

**Symptom**: Save button doesn't work or errors

**Solutions**:
1. Verify user is authenticated (JWT present)
2. Check station is in cache (search first)
3. Review network tab for API errors
4. Check backend logs for issues

### Search Returns No Results

**Symptom**: Search completes but no stations shown

**Solutions**:
1. Verify location is correct (lat/lng valid)
2. Try larger radius (default 5km may be too small)
3. Check Google Maps API quota (not exceeded)
4. Review backend circuit breaker state

## References

- Backend API: `/backend/src/features/stations/docs/API.md`
- Backend Architecture: `/backend/src/features/stations/docs/ARCHITECTURE.md`
- Testing Guide: `/backend/src/features/stations/docs/TESTING.md`
- Google Maps Setup: `/backend/src/features/stations/docs/GOOGLE-MAPS-SETUP.md`
- Runtime Config: `/frontend/docs/RUNTIME-CONFIG.md`
- Main README: `/backend/src/features/stations/README.md`
