Files
ridebus-pwa/src/components/NearbyStops.tsx
Zakhar Timoshenko 1492f179eb feat: add new components for departures, discovery, and navigation
- Implemented SCSS styles for compact board in departures.
- Created styles for feature discovery modal and its elements.
- Added navigation styles for desktop and mobile views.
- Developed FeatureDiscovery component with guided steps for user onboarding.
- Introduced freshness utility to manage data freshness based on timestamps.
- Added tests for freshness utility to ensure correct behavior across various scenarios.
2026-10-07 21:51:05 +03:00

46 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import { loadNearby } from '../lib/api';
import { groupStops } from '../lib/stopGroups';
import { MapPin, RefreshCw } from './Icons';
import { StopList, StopListItem } from './StopList';
import type { NearbyStop, Stop } from '../types';
export function NearbyStops({ cityId, mode, online, onSelect, selectedGroupKey, favoriteStopIds, autoLocate = false }: { cityId: number; mode: 'live' | 'demo'; online: boolean; onSelect: (stop: Stop) => void; selectedGroupKey?: string; favoriteStopIds: number[]; autoLocate?: boolean }) {
const [results, setResults] = useState<NearbyStop[] | null>(null);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState('');
const [approximate, setApproximate] = useState(false);
const request = useRef(0);
const controller = useRef<AbortController | null>(null);
useEffect(() => () => { request.current++; controller.current?.abort(); }, []);
useEffect(() => {
if (!autoLocate) return;
const timer = setTimeout(() => void searchNearby(), 0);
return () => clearTimeout(timer);
}, [autoLocate]);
async function searchNearby() {
if (!online || busy) return;
if (!navigator.geolocation || !window.isSecureContext) { setMessage('Поиск по местоположению недоступен в этом браузере. Найдите остановку по названию.'); return; }
const current = ++request.current;
controller.current?.abort(); controller.current = new AbortController();
const signal = controller.current.signal;
setBusy(true); setMessage('');
try {
const position = await new Promise<GeolocationPosition>((resolve, reject) => navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 }));
if (current !== request.current) return;
const stops = await loadNearby(mode, cityId, position.coords.latitude, position.coords.longitude, signal);
if (current !== request.current) return;
setResults(stops); setApproximate(position.coords.accuracy > 300);
} catch (error) {
if (current !== request.current) return;
const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
setMessage(code === 1 ? 'Доступ к местоположению закрыт. Можно выбрать остановку по названию.' : code === 3 ? 'Не удалось определить местоположение вовремя. Попробуйте ещё раз или найдите остановку по названию.' : 'Не удалось найти остановки рядом. Попробуйте ещё раз или воспользуйтесь поиском.');
} finally { if (current === request.current) setBusy(false); }
}
const groups = groupStops(results ?? []).map(group => ({ group, nearest: (results ?? []).filter(stop => group.stops.some(item => item.id === stop.id)).sort((a, b) => a.distance - b.distance)[0] })).sort((a, b) => a.nearest.distance - b.nearest.distance);
return <div className="nearbySection"><button className="secondaryButton nearbyTrigger" disabled={!online || busy} onClick={() => void searchNearby()}>{busy ? <RefreshCw size={18} className="spinning" /> : <MapPin size={18} />}{busy ? 'Ищем рядом…' : results ? 'Обновить поиск рядом' : 'Остановки рядом'}</button>
{message && <p className="nearbyMessage" role="status">{message}</p>}
{results && <div className="nearbyResults"><p>{mode === 'demo' ? 'Примеры остановок рядом' : 'В радиусе 1 км'}{approximate && ' · местоположение приблизительное'}</p><StopList label="Остановки рядом">{!groups.length ? <div className="listMessage" role="status">Рядом нет остановок выбранного города. Попробуйте выбрать другой город.</div> : groups.map(({ group, nearest }) => <StopListItem key={group.key} name={group.name} selected={selectedGroupKey === group.key} favorite={group.stops.some(stop => favoriteStopIds.includes(stop.id))} distance={nearest.distance} onSelect={() => onSelect(nearest)} />)}</StopList></div>}
</div>;
}