diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..0b8d220e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,64 @@
+.env
+.env.local
+.env.*.local
+
+node_modules/
+package-lock.json
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+.DS_Store
+.AppleDouble
+.LSOverride
+._*
+
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+$RECYCLE.BIN/
+
+*~
+.fuse_hidden*
+
+.vscode/
+
+.vscode/.history
+
+.idea/
+*.swp
+*.swo
+*~
+.project
+.classpath
+.settings/
+
+*.log
+logs/
+
+tmp/
+temp/
+.cache/
+
+coverage/
+.nyc_output/
+*.lcov
+
+*.pid
+*.seed
+*.pid.lock
+
+dist/
+build/
+out/
+
+*.key
+*.pem
+*.crt
+*.p12
+*.pfx
+
+*password*
+*secret*
+*token*
\ No newline at end of file
diff --git a/README.md b/README.md
index 163d41b9..88fd4766 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,35 @@
-# Безопасность веб-приложений. Лабораторка №2
+# Лабораторная работа №2
-## Схема сдачи
+## О лабораторной работе
-1. Получить задание
-2. Сделать форк данного репозитория
-3. Выполнить задание согласно полученному варианту
-4. Сделать PR (pull request) в данный репозиторий
-6. Исправить замечания после code review
-7. Получить approve
-8. Прийти на занятие и защитить работу
+Это лабораторная работа №2 по дисциплине «Безопасность веб-приложений». По варианту 2 реализован веб-аналог «Прибывалки» для электричек: поиск станций, просмотр расписания по станции, выбор любимых станций, просмотр маршрутов проходящих между станций и через станции и адаптация интерфейса под мобильные устройства.
-Что нужно проявить в работе:
-- умение разработать завершенное целое веб-приложение, с клиентской и серверной частями (допустимы открытые АПИ)
-- навыки верстки на html в объеме 200-300 тегов
-- навыки применения css для лейаута и стилизации, желательно с адаптацией к мобилке
-- использование jQuery или аналогичных JS-фреймворков
-- динамическая подгрузка контента
-- динамическое изменение DOM и CSSOM
-
-Если у вас своя идея по заданию, то расскажите, обсудим и подкорректирую.
-
-## Вариант 1. Расписания
-
-Сделать аналог раздела https://ssau.ru/rasp?groupId=531030143
-
-Какие нужны возможности:
-- справочники групп, табличные данные по расписаниям добывать с настоящего сайта на серверной стороне приложения
-- в клиентскую часть подгружать эти сведения динамически по JSON-API
-- обеспечить возможность смотреть расписания в разрезе группы или препода
-- обеспечить возможность выбора учебной недели (по умолчанию выбирается автоматически)
-
-## Вариант 2. Аналог Прибывалки для электричек
-
-Сделать веб-версию Прибывалки, только для электричек
-
-Какие нужны возможности:
-- находить желаемую ЖД-станцию поиском по названию и по карте
-- отображать расписания всех проходящих поездов через выбранную станцию
-- отображать расписания для поездов между двумя станциями
-- работа через АПИ Яндекс.Расписаний https://yandex.ru/dev/rasp/doc/ru/ (доступ получите сами)
-- хорошая работа в условиях экрана смартфона
-- бонус: функция "любимых остановок"
-
-## Вариант 3. Прогноз погоды
-
-Сделать одностраничный сайт с картой, на которой можно выбрать населенный пункт и получить прогноз погоды на несколько дней по нему.
-
-Какие нужны возможности:
- - увидеть на карте точки с населенными пунктами. Координаты населенных пунктов взять из https://tochno.st/datasets/allsettlements - но все 150 тысяч не нужно, выберите 1 тысячу с самым большим населением.
- - при нажатии на точку получить всплывающее окошко с графиками изменения температуры, осадков, силы ветра. API для прогнозов возьмите с https://projecteol.ru/ru/ с соблюдением правил.
- - графики рисовать каким-нибудь приличным компонентом, например, https://www.chartjs.org/
- - находить населенный пункт по названию
- - можете реализовать с собственным серверным компонентом или придумать, как обойтись без него
+## Что реализовано
+- Поиск железнодорожных станций по названию и выбор станций на карте.
+- Просмотр расписания всех проходящих через выбранную станцию поездов.
+- Поиск маршрутов между станций и через станции.
+- Избранные станции с сохранением в localStorage.
+- Адаптивный интерфейс для смартфонов и десктопа.
+## Технологии
+- Frontend: React, Vite, React Router, React Leaflet, CSS.
+- Backend: Node.js, Express, Axios, CORS, dotenv.
+- Внешний источник данных: API Яндекс.Расписаний.
+- Пакетный менеджер: npm.
+## Как запустить проект
+1. Установите зависимости для backend: `npm install` в папке backend.
+2. Создайте файл `backend/.env` и добавьте ключ: `YANDEX_RASP_API_KEY=ваш_ключ`. При необходимости добавьте `PORT=5000`.
+3. Запустите backend: `npm run dev`.
+4. Установите зависимости для frontend: `npm install` в папке frontend.
+5. Запустите frontend: `npm run dev`.
+6. Откройте адрес из вывода Vite в браузере (обычно `http://localhost:5173`).
+## Структура приложения
+- Вкладка «По станции»: загрузка расписания для выбранной станции.
+- Вкладка «Маршрут»: электрички между станциями отправления и прибытия/целиком проходящие через этот маршрут.
+- Backend API: `/api/search-station`, `/api/station-schedule`, `/api/train-route`.
\ No newline at end of file
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 00000000..fe8573de
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "train-arrival-backend",
+ "version": "1.0.0",
+ "description": "Backend for train schedule app",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "axios": "^1.6.0",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.0",
+ "express": "^4.18.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.14"
+ }
+}
diff --git a/backend/routes/routes.js b/backend/routes/routes.js
new file mode 100644
index 00000000..7ac0ab83
--- /dev/null
+++ b/backend/routes/routes.js
@@ -0,0 +1,16 @@
+const express = require('express');
+const router = express.Router();
+const { getRoutes } = require('../services/yandexApi');
+
+router.get('/', async (req, res) => {
+ try {
+ const from = req.query.from;
+ const to = req.query.to;
+ const routes = await getRoutes(from, to);
+ res.json(routes);
+ } catch (error) {
+ res.status(500).json({ error: 'Ошибка получения маршрута' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/schedule.js b/backend/routes/schedule.js
new file mode 100644
index 00000000..19941004
--- /dev/null
+++ b/backend/routes/schedule.js
@@ -0,0 +1,21 @@
+const express = require('express');
+const router = express.Router();
+const { getSchedule } = require('../services/yandexApi');
+const { formatSchedule } = require('../utils/formatters');
+
+router.get('/', async (req, res) => {
+ try {
+ const stationCode = req.query.station;
+ if (!stationCode) {
+ return res.status(400).json({ error: 'Параметр station обязателен' });
+ }
+
+ const schedule = await getSchedule(stationCode);
+ const formatted = formatSchedule(schedule);
+ res.json(formatted);
+ } catch (error) {
+ res.status(500).json({ error: error.message || 'Ошибка получения расписания' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/stations.js b/backend/routes/stations.js
new file mode 100644
index 00000000..8110c41a
--- /dev/null
+++ b/backend/routes/stations.js
@@ -0,0 +1,36 @@
+const express = require('express');
+const router = express.Router();
+const { searchStations, getStationsInBounds } = require('../services/yandexApi');
+
+router.get('/', async (req, res) => {
+ try {
+ const query = req.query.query || '';
+ const zoom = Number(req.query.zoom);
+ const minLat = Number(req.query.minLat);
+ const minLng = Number(req.query.minLng);
+ const maxLat = Number(req.query.maxLat);
+ const maxLng = Number(req.query.maxLng);
+
+ const hasBounds =
+ Number.isFinite(minLat) &&
+ Number.isFinite(minLng) &&
+ Number.isFinite(maxLat) &&
+ Number.isFinite(maxLng);
+
+ if (hasBounds) {
+ if (!Number.isFinite(zoom) || zoom < 9) {
+ return res.json([]);
+ }
+ const stations = await getStationsInBounds({ minLat, minLng, maxLat, maxLng });
+ return res.json(stations);
+ }
+
+ const stations = await searchStations(query);
+ return res.json(stations);
+ } catch (error) {
+ console.error('Ошибка поиска станции:', error.message);
+ res.status(500).json({ error: error.message });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 00000000..02aede98
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,27 @@
+require('dotenv').config();
+const express = require('express');
+const cors = require('cors');
+
+
+const stationRoutes = require('./routes/stations');
+const scheduleRoutes = require('./routes/schedule');
+const routeRoutes = require('./routes/routes');
+
+const app = express();
+
+app.use(cors());
+app.use(express.json());
+
+app.use('/api/search-station', stationRoutes);
+app.use('/api/station-schedule', scheduleRoutes);
+app.use('/api/train-route', routeRoutes);
+
+app.get('/', (req, res) => {
+ res.send('API Прибывалка.Электрички работает');
+});
+
+const PORT = process.env.PORT || 5000;
+
+app.listen(PORT, () => {
+ console.log(`Server started on port ${PORT}`);
+});
\ No newline at end of file
diff --git a/backend/services/yandexApi.js b/backend/services/yandexApi.js
new file mode 100644
index 00000000..ab362747
--- /dev/null
+++ b/backend/services/yandexApi.js
@@ -0,0 +1,152 @@
+const axios = require('axios');
+
+const API_KEY = process.env.YANDEX_RASP_API_KEY;
+const BASE_URL = 'https://api.rasp.yandex.net/v3.0/';
+const STATIONS_CACHE_TTL_MS = 10 * 60 * 1000;
+let stationsCache = {
+ ts: 0,
+ data: []
+};
+
+function normalizeText(value = '') {
+ return value.toLowerCase().trim().replaceAll('ё', 'е');
+}
+
+function getStationCoords(station) {
+ let lat = Number(station.latitude ?? station.lat ?? station.point?.lat);
+ let lng = Number(station.longitude ?? station.lng ?? station.lon ?? station.point?.lng);
+
+ if (Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) > 90 && Math.abs(lng) <= 90) {
+ const swappedLat = lng;
+ const swappedLng = lat;
+ lat = swappedLat;
+ lng = swappedLng;
+ }
+
+ return {
+ lat: Number.isFinite(lat) ? lat : null,
+ lng: Number.isFinite(lng) ? lng : null
+ };
+}
+
+function scoreStationMatch(title, query) {
+ const stationTitle = normalizeText(title);
+ const q = normalizeText(query);
+
+ if (stationTitle === q) return 100;
+ if (stationTitle.startsWith(`${q} `) || stationTitle.startsWith(`${q}-`)) return 95;
+ if (stationTitle.startsWith(q)) return 90;
+ if (stationTitle.split(/[\s-]+/).some(word => word.startsWith(q))) return 75;
+ if (stationTitle.includes(q)) return 50;
+ return 0;
+}
+
+async function getFlatStations() {
+ const cacheIsFresh = Date.now() - stationsCache.ts < STATIONS_CACHE_TTL_MS;
+ if (cacheIsFresh && stationsCache.data.length > 0) {
+ return stationsCache.data;
+ }
+
+ const url = `${BASE_URL}stations_list/?apikey=${API_KEY}&format=json&lang=ru_RU&transport_types=train`;
+ const response = await axios.get(url);
+ const uniqueStations = new Map();
+
+ const addStation = ({ station, regionTitle, cityTitle }) => {
+ if (!station || station.transport_type !== 'train' || !station.title) return;
+ const code = station.codes?.yandex_code;
+ if (!code) return;
+
+ const { lat, lng } = getStationCoords(station);
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
+
+ const locationParts = [cityTitle, regionTitle].filter(Boolean);
+ const location = locationParts.join(', ');
+ const displayTitle = location ? `${station.title} (${location})` : station.title;
+
+ if (!uniqueStations.has(code)) {
+ uniqueStations.set(code, {
+ title: station.title,
+ displayTitle,
+ location,
+ code,
+ lat,
+ lng
+ });
+ }
+ };
+
+ (response.data.countries || []).forEach(country => {
+ (country.regions || []).forEach(region => {
+ (region.stations || []).forEach(station =>
+ addStation({ station, regionTitle: region.title, cityTitle: '' })
+ );
+ (region.settlements || []).forEach(city => {
+ (city.stations || []).forEach(station =>
+ addStation({ station, regionTitle: region.title, cityTitle: city.title })
+ );
+ });
+ });
+ });
+
+ stationsCache = {
+ ts: Date.now(),
+ data: Array.from(uniqueStations.values())
+ };
+
+ return stationsCache.data;
+}
+
+async function searchStations(query) {
+ const stations = await getFlatStations();
+ const normalizedQuery = normalizeText(query);
+
+ if (!normalizedQuery) {
+ return stations;
+ }
+
+ return stations
+ .map(station => ({
+ ...station,
+ score: Math.max(
+ scoreStationMatch(station.title, normalizedQuery),
+ scoreStationMatch(station.displayTitle, normalizedQuery)
+ )
+ }))
+ .filter(station => station.score > 0)
+ .sort((a, b) => b.score - a.score || a.displayTitle.localeCompare(b.displayTitle, 'ru'))
+ .slice(0, 40)
+ .map(({ score, ...station }) => station);
+}
+
+async function getStationsInBounds({ minLat, minLng, maxLat, maxLng, limit = 600 }) {
+ const stations = await getFlatStations();
+
+ return stations
+ .filter(
+ station =>
+ station.lat >= minLat &&
+ station.lat <= maxLat &&
+ station.lng >= minLng &&
+ station.lng <= maxLng
+ )
+ .slice(0, limit);
+}
+
+async function getSchedule(stationCode) {
+ const url = `${BASE_URL}schedule/?apikey=${API_KEY}&station=${stationCode}&lang=ru_RU&transport_types=train`;
+ const response = await axios.get(url);
+ return response.data.schedule;
+}
+
+async function getRoutes(from, to) {
+ const url = `${BASE_URL}search/?apikey=${API_KEY}&from=${from}&to=${to}&lang=ru_RU&transport_types=train`;
+ const response = await axios.get(url);
+ return response.data.segments;
+}
+
+module.exports = {
+ searchStations,
+ getStationsInBounds,
+ getSchedule,
+ getRoutes
+};
\ No newline at end of file
diff --git a/backend/utils/formatters.js b/backend/utils/formatters.js
new file mode 100644
index 00000000..f365f40c
--- /dev/null
+++ b/backend/utils/formatters.js
@@ -0,0 +1,40 @@
+function formatSchedule(schedule) {
+ if (!Array.isArray(schedule)) {
+ return [];
+ }
+
+ const seen = new Set();
+
+ return schedule
+ .map(item => {
+ return {
+ departure: item?.departure || '—',
+ arrival: item?.arrival || '—',
+ train: item?.thread?.title || '—',
+ direction: item?.thread?.short_title || item?.thread?.title || '—',
+ platform: item?.platform || '—',
+ status: item?.except_days || ''
+ };
+ })
+ .filter(item => {
+ const key = [
+ item.departure,
+ item.arrival,
+ item.train,
+ item.direction,
+ item.platform,
+ item.status
+ ].join('|');
+
+ if (seen.has(key)) {
+ return false;
+ }
+
+ seen.add(key);
+ return true;
+ });
+}
+
+module.exports = {
+ formatSchedule
+};
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000..a36934d8
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 00000000..4fa125da
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 00000000..38162c9c
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Прибывалка.Электрички
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 00000000..1e9216fc
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "leaflet": "^1.9.4",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-leaflet": "^5.0.0",
+ "react-router-dom": "^7.14.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "vite": "^8.0.4"
+ }
+}
diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg
new file mode 100644
index 00000000..e9522193
--- /dev/null
+++ b/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 00000000..1b56ecbe
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,23 @@
+import { Routes, Route } from 'react-router-dom';
+import Header from './layout/Header';
+
+import StationPage from './pages/StationPage';
+import RoutePage from './pages/RoutePage';
+//import FavoritesPage from './pages/FavoritesPage';
+import AboutPage from './pages/AboutPage';
+
+export default function App() {
+ return (
+ <>
+
+
+
+ } />
+ } />
+ } />
+ {/*} />*/}
+ } />
+
+ >
+ );
+}
diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js
new file mode 100644
index 00000000..062b73ae
--- /dev/null
+++ b/frontend/src/api/api.js
@@ -0,0 +1,41 @@
+const BASE = 'http://localhost:5000/api';
+
+async function requestJson(url) {
+ const res = await fetch(url);
+ const payload = await res.json();
+
+ if (!res.ok) {
+ throw new Error(payload?.error || 'Ошибка запроса');
+ }
+
+ return payload;
+}
+
+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
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
new file mode 100644
index 00000000..7c1e84e5
--- /dev/null
+++ b/frontend/src/components/Navbar.jsx
@@ -0,0 +1,12 @@
+import { Link } from 'react-router-dom';
+
+export default function Navbar() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/ScheduleCard.jsx b/frontend/src/components/ScheduleCard.jsx
new file mode 100644
index 00000000..115d68f9
--- /dev/null
+++ b/frontend/src/components/ScheduleCard.jsx
@@ -0,0 +1,21 @@
+export default function ScheduleCard({ train }) {
+ return (
+
+
+ {train.direction}
+
+
+
+ Отправление: {train.departure}
+
+
+
+ Поезд: {train.train}
+
+
+
+ Платформа: {train.platform}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/ScheduleTable.jsx b/frontend/src/components/ScheduleTable.jsx
new file mode 100644
index 00000000..4ccba54d
--- /dev/null
+++ b/frontend/src/components/ScheduleTable.jsx
@@ -0,0 +1,22 @@
+export default function ScheduleTable({ data }) {
+ return (
+
+ );
+}
diff --git a/frontend/src/context/AppContext.jsx b/frontend/src/context/AppContext.jsx
new file mode 100644
index 00000000..92bf8bc8
--- /dev/null
+++ b/frontend/src/context/AppContext.jsx
@@ -0,0 +1,13 @@
+import { useState } from 'react';
+import { AppContext } from './AppContextInstance';
+export { AppContext } from './AppContextInstance';
+
+export function AppProvider({ children }) {
+ const [schedule, setSchedule] = useState([]);
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/context/AppContextInstance.js b/frontend/src/context/AppContextInstance.js
new file mode 100644
index 00000000..e9220de9
--- /dev/null
+++ b/frontend/src/context/AppContextInstance.js
@@ -0,0 +1,3 @@
+import { createContext } from 'react';
+
+export const AppContext = createContext();
diff --git a/frontend/src/layout/Container.jsx b/frontend/src/layout/Container.jsx
new file mode 100644
index 00000000..a9f2181b
--- /dev/null
+++ b/frontend/src/layout/Container.jsx
@@ -0,0 +1,3 @@
+export default function Container({ children }) {
+ return
{children}
;
+}
\ No newline at end of file
diff --git a/frontend/src/layout/Header.jsx b/frontend/src/layout/Header.jsx
new file mode 100644
index 00000000..275bae4c
--- /dev/null
+++ b/frontend/src/layout/Header.jsx
@@ -0,0 +1,31 @@
+import { NavLink } from 'react-router-dom';
+
+export default function Header() {
+ const navItems = [
+ { to: '/', label: 'По станции' },
+ { to: '/route', label: 'Маршрут' },
+ { to: '/about', label: 'О сайте' }
+ ];
+
+ return (
+
+
+
Прибывалка.Электрички
+
Расписание пригородных поездов
+
+
+
+
+ );
+}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 00000000..096e9a8a
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import { BrowserRouter } from 'react-router-dom'
+import { AppProvider } from './context/AppContext.jsx'
+
+import './styles/main.css'
+import './styles/cards.css'
+import './styles/mobile.css'
+import './styles/styles.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+)
\ No newline at end of file
diff --git a/frontend/src/pages/AboutPage.jsx b/frontend/src/pages/AboutPage.jsx
new file mode 100644
index 00000000..9c8b34e4
--- /dev/null
+++ b/frontend/src/pages/AboutPage.jsx
@@ -0,0 +1,61 @@
+import Container from '../layout/Container';
+
+export default function AboutPage() {
+ return (
+
+
+
О лабораторной работе
+
+ Это лабораторная работа №2 по дисциплине «Безопасность веб-приложений». По варианту 2
+ реализован веб-аналог «Прибывалки» для электричек: поиск станций, просмотр расписания по
+ станции, выбор любимых станций, просмотр маршрутов проходящих между станций и через станции и адаптация интерфейса под мобильные устройства.
+
+
+
+
Что реализовано
+
+
Поиск железнодорожных станций по названию и выбор станций на карте.
+
Просмотр расписания всех проходящих через выбранную станцию поездов.