Conversation
| "dev": "nodemon server.js" | ||
| }, | ||
| "dependencies": { | ||
| "axios": "^1.6.0", |
There was a problem hiding this comment.
не совсем рекомендую использовать axios
| const BASE_URL = 'https://api.rasp.yandex.net/v3.0/'; | ||
| const STATIONS_CACHE_TTL_MS = 10 * 60 * 1000; |
| @@ -0,0 +1,41 @@ | |||
| const BASE = 'http://localhost:5000/api'; | |||
| export async function searchStation(query) { | ||
| const data = await requestJson(`${BASE}/search-station?query=${encodeURIComponent(query)}`); | ||
| return Array.isArray(data) ? data : []; | ||
| } | ||
|
|
||
| export async function getStationsInBounds(bounds) { | ||
| const params = new URLSearchParams({ | ||
| minLat: String(bounds.minLat), | ||
| minLng: String(bounds.minLng), | ||
| maxLat: String(bounds.maxLat), | ||
| maxLng: String(bounds.maxLng), | ||
| zoom: String(bounds.zoom) | ||
| }); | ||
| const data = await requestJson(`${BASE}/search-station?${params.toString()}`); | ||
| return Array.isArray(data) ? data : []; | ||
| } | ||
|
|
||
| export async function getSchedule(code) { | ||
| const data = await requestJson(`${BASE}/station-schedule?station=${encodeURIComponent(code)}`); | ||
| return Array.isArray(data) ? data : []; | ||
| } | ||
|
|
||
| export async function getRoutes(from, to) { | ||
| const data = await requestJson( | ||
| `${BASE}/train-route?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}` | ||
| ); | ||
| return Array.isArray(data) ? data : []; | ||
| } No newline at end of file |
There was a problem hiding this comment.
Сборка итогового url для запроса, должна выполнять функция requestJson, остальные, кто вызывают её должны просто отдавать объекты туда предварительно валидировав данные (условно проверять, что при запросе станции по esr_code у вас код точно состоит из N цифр и не содержит букв)
| if (inputRef.current) { | ||
| const rect = inputRef.current.getBoundingClientRect(); | ||
| setDropdownStyle({ |
There was a problem hiding this comment.
if (!inputRef.current) return;
/* остальной код */| let dropdownContent = null; | ||
| if (loading) { | ||
| dropdownContent = <div className="autocomplete-loading-item">Поиск…</div>; | ||
| } else if (suggestions.length === 0 && value.trim() !== '') { | ||
| dropdownContent = <div className="autocomplete-empty-item">Ничего не найдено</div>; | ||
| } else if (suggestions.length > 0) { | ||
| dropdownContent = suggestions.map((station, idx) => ( | ||
| <button | ||
| key={station.code || idx} | ||
| type="button" | ||
| className="autocomplete-item" | ||
| onClick={() => handleSelect(station)} | ||
| > | ||
| {station.displayTitle || station.title} | ||
| </button> | ||
| )); | ||
| } |
There was a problem hiding this comment.
Вот это лучше организовать как функцию типо renderDropDown
| const DEFAULT_CENTER = [53.1959, 50.1008]; | ||
| const DEFAULT_ZOOM = 10; |
There was a problem hiding this comment.
Либо в .env, либо в файл конфига
| @@ -0,0 +1,122 @@ | |||
| import { CircleMarker, MapContainer, Popup, TileLayer, useMapEvents } from 'react-leaflet'; | |||
| import { useEffect } from 'react'; | |||
| import 'leaflet/dist/leaflet.css'; | |||
There was a problem hiding this comment.
Не советую использовать Leaflet, так как это достаточно низкоуровневая библиотека для работы с картой (читать про OpenLayers)
| const timerId = setTimeout(async () => { | ||
| try { | ||
| const stations = await searchStation(value); | ||
| if (requestId !== fromRequestId.current) return; | ||
| setFromStations(stations); | ||
| if (stations.length === 0) setMessage('Станция отправления не найдена.'); | ||
| } catch { | ||
| if (requestId !== fromRequestId.current) return; | ||
| setFromStations([]); | ||
| setMessage('Ошибка поиска станции отправления.'); | ||
| } finally { | ||
| if (requestId === fromRequestId.current) setFromLoading(false); | ||
| } | ||
| }, 350); | ||
|
|
||
| return () => clearTimeout(timerId); |
There was a problem hiding this comment.
Честно говоря затрудняюсь сказать, что вы хотели этим сделать, выглядит как некий debounce, но если что, такие функции уже готовые и реализованные уже есть
| const timerId = setTimeout(async () => { | ||
| try { | ||
| const stations = await searchStation(value); | ||
| if (requestId !== toRequestId.current) return; | ||
| setToStations(stations); | ||
| if (stations.length === 0) setMessage('Станция прибытия не найдена.'); | ||
| } catch { | ||
| if (requestId !== toRequestId.current) return; | ||
| setToStations([]); | ||
| setMessage('Ошибка поиска станции прибытия.'); | ||
| } finally { | ||
| if (requestId === toRequestId.current) setToLoading(false); | ||
| } | ||
| }, 350); |
| setTimeout(async () => { | ||
| if (requestId !== viewportRequestId.current) return; | ||
| try { | ||
| const stations = await getStationsInBounds(viewport); | ||
| if (requestId !== viewportRequestId.current) return; | ||
| setMapStations(stations); | ||
| } catch { | ||
| if (requestId !== viewportRequestId.current) return; | ||
| setMapStations([]); | ||
| } | ||
| }, 220); |
There was a problem hiding this comment.
Благодаря вот такой штуке у вас может произойти момент с тем, что вы превысите количество запросов по ключу к API яндекса, если же реализовывать такие вещи в реальном продакшене, где настроено ограничение запросов, то вы будете ловить 429, что негативно скажется на UX.
Самое логичное решение, исходя из вашего желания сделать перезапрос при изменении позиционирования на карте, состоит аж из 2 вариантов:
- Запросить один раз все станции и отрисовать их все сразу (тот же OpenLayers не рендерит те объекты, которые не видно)
- Выполнять запрос через
debounce. Т.е. если меняется видимая зона, то ставиться таймер на условные 1.5 секунды и если в течении этого времени поле видимости не поменялось, то выполнять запрос
| const FAVORITES_STORAGE_KEY = 'favoriteStationSchedules'; | ||
|
|
||
| function loadFavoritesFromStorage() { | ||
| if (typeof window === 'undefined') { | ||
| return []; | ||
| } | ||
|
|
||
| try { | ||
| const raw = window.localStorage.getItem(FAVORITES_STORAGE_KEY); | ||
| if (!raw) { | ||
| return []; | ||
| } | ||
|
|
||
| const parsed = JSON.parse(raw); | ||
| if (!Array.isArray(parsed)) { | ||
| return []; | ||
| } | ||
|
|
||
| return parsed | ||
| .filter( | ||
| item => | ||
| item && | ||
| typeof item === 'object' && | ||
| item.station && | ||
| typeof item.station === 'object' && | ||
| typeof item.station.code === 'string' | ||
| ) | ||
| .map(item => ({ station: item.station })); | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| function saveFavoritesToStorage(favorites) { | ||
| if (typeof window === 'undefined') { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| window.localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(favorites)); | ||
| } catch { | ||
| // Ignore storage errors (private mode, quota, etc.) so schedule page stays usable. | ||
| } | ||
| } |
There was a problem hiding this comment.
Всю логику работы с localStorage вынести в отдельный файл.
Проверять доступность localStorage лучше вот так
| useEffect(() => { | ||
| const value = query.trim(); | ||
| if (!value) { | ||
| setSuggestions([]); | ||
| setSearchLoading(false); | ||
| return undefined; | ||
| } | ||
|
|
||
| const requestId = ++searchRequestId.current; | ||
| setSearchLoading(true); | ||
| setMessage(''); | ||
|
|
||
| const timerId = setTimeout(async () => { | ||
| try { | ||
| const foundStations = await searchStation(value); | ||
| if (requestId !== searchRequestId.current) return; | ||
| setSuggestions(foundStations); | ||
| if (foundStations.length === 0) { | ||
| setMessage('Станции не найдены.'); | ||
| } | ||
| } catch { | ||
| if (requestId !== searchRequestId.current) return; | ||
| setSuggestions([]); | ||
| setMessage('Не удалось выполнить поиск станции.'); | ||
| } finally { | ||
| if (requestId === searchRequestId.current) { | ||
| setSearchLoading(false); | ||
| } | ||
| } | ||
| }, 350); | ||
|
|
||
| return () => clearTimeout(timerId); | ||
| }, [query]); | ||
|
|
||
| const handleViewportChange = viewport => { | ||
| const requestId = ++viewportRequestId.current; | ||
| setTimeout(async () => { | ||
| if (requestId !== viewportRequestId.current) return; | ||
| try { | ||
| const stations = await getStationsInBounds(viewport); | ||
| if (requestId !== viewportRequestId.current) return; | ||
| setMapStations(stations); | ||
| } catch { | ||
| if (requestId !== viewportRequestId.current) return; | ||
| setMapStations([]); | ||
| } | ||
| }, 220); | ||
| }; |
There was a problem hiding this comment.
Аналогично про debounce и частоту запросов
| <Route path="/" element={<StationPage />} /> | ||
| <Route path="/station" element={<StationPage />} /> | ||
| <Route path="/route" element={<RoutePage />} /> | ||
| {/*<Route path="/favorites" element={<FavoritesPage />} />*/} |
There was a problem hiding this comment.
Если не используете, можно удалить
Это лабораторная работа №2 по дисциплине «Безопасность веб-приложений». По варианту 2 реализован веб-аналог «Прибывалки» для электричек: поиск станций, просмотр расписания по станции, выбор любимых станций, просмотр маршрутов проходящих между станций и через станции и адаптация интерфейса под мобильные устройства. Реализовано при помощи технологий React, Node.js и др. Более полное описание и инструкция по запуску в README.md.