- 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.
46 lines
4.3 KiB
TypeScript
46 lines
4.3 KiB
TypeScript
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>;
|
||
}
|