diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7ceb43c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Flask +instance/ +.webassets-cache +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +desktop.ini + +# Логи и базы данных +*.log +*.sql +*.sqlite +*.db + +# Кэш станций (создается автоматически при запуске) +backend/stations_cache.json +backend/__pycache__/ + +# Виртуальное окружение +backend/venv/ +backend/env/ +backend/venv*/ + +# Файлы с секретами +backend/.env +.secrets + +# Временные файлы +*.tmp +*.temp +*.bak +*.backup + +# Результаты тестов +.coverage +htmlcov/ +.pytest_cache/ +.tox/ + +# Файлы зависимостей (они уже есть в requirements.txt) +# Не удаляем сам requirements.txt + +# Node modules (если появятся) +node_modules/ +npm-debug.log + +# Системные файлы Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Системные файлы macOS +.AppleDouble +.LSOverride +._* + +# Файлы отладки +*.pid +*.seed +*.pid.lock + +# Secrets +backend/.env +.env +*.env + +# Python +__pycache__/ +*.py[cod] +venv/ +env/ + +# Cache +electric-train-tracker/backend/stations_cache.json + +# IDE +.vscode/ +.idea/ + +# Yandex schedule cache +yaschedule.core.cache +*.cache +backend/yaschedule.core.cache + +# Temporary files +*.tmp +*.temp +*.cache \ No newline at end of file diff --git a/electric-train-tracker/backend/requirements.txt b/electric-train-tracker/backend/requirements.txt new file mode 100644 index 00000000..051d8e4b --- /dev/null +++ b/electric-train-tracker/backend/requirements.txt @@ -0,0 +1,5 @@ +flask==2.3.3 +flask-cors==4.0.0 +requests==2.31.0 +yaschedule==0.0.4.2 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/electric-train-tracker/backend/server.py b/electric-train-tracker/backend/server.py new file mode 100644 index 00000000..6069a82b --- /dev/null +++ b/electric-train-tracker/backend/server.py @@ -0,0 +1,143 @@ +import os +from flask import Flask, request, jsonify, send_from_directory +from flask_cors import CORS +from yaschedule.core import YaSchedule +from station_cache import load_all_stations, search_stations +from datetime import datetime +import requests +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__, static_folder='../frontend/static', static_url_path='/static') +CORS(app) + +API_KEY = os.getenv('YANDEX_API_KEY') +FLASK_DEBUG = os.getenv('FLASK_DEBUG', 'True') == 'True' +PORT = int(os.getenv('PORT', 5000)) + +if not API_KEY: + print("❌ ОШИБКА: YANDEX_API_KEY не найден в .env файле!") + print("Создайте файл .env в папке backend и добавьте:") + print("YANDEX_API_KEY=ваш_ключ_здесь") + exit(1) + +print(f"✅ API ключ загружен из .env") + +train_api = YaSchedule(API_KEY) + +print("📡 Загрузка базы станций...") +station_db = load_all_stations(API_KEY) +print("✅ База готова!") + +@app.route('/') +def serve_index(): + return send_from_directory('../frontend/pages', 'index.html') + +@app.route('/api/search/stations', methods=['GET']) +def search_stations_by_name(): + search_term = request.args.get('q', '') + if len(search_term) < 2: + return jsonify([]) + + results = search_stations(station_db, search_term) + return jsonify(results) + +@app.route('/api/geo/nearby', methods=['GET']) +def get_nearby_railway_stations(): + lat = request.args.get('lat') + lng = request.args.get('lng') + distance = request.args.get('distance', 50) + + if not lat or not lng: + return jsonify([]) + + try: + url = "https://api.rasp.yandex.net/v3.0/nearest_stations/" + params = { + 'apikey': API_KEY, + 'lat': lat, + 'lng': lng, + 'distance': distance, + 'transport_types': 'train,suburban', + 'station_types': 'train_station', + 'lang': 'ru_RU', + 'limit': 20 + } + + response = requests.get(url, params=params, timeout=10) + if response.status_code == 200: + data = response.json() + stations = [] + for station in data.get('stations', []): + if station.get('transport_type') in ['train', 'suburban']: + stations.append({ + 'title': station.get('title'), + 'code': station.get('code'), + 'distance': station.get('distance'), + 'latitude': station.get('lat'), + 'longitude': station.get('lng'), + 'city': station.get('city', '') + }) + return jsonify(stations) + else: + return jsonify([]) + except Exception as e: + print(f"Error: {e}") + return jsonify([]) + +@app.route('/api/recommended/stations', methods=['GET']) +def get_recommended_stations(): + popular_stations = [ + {'name': 'Москва (Киевский вокзал)', 'code': 's9603402'}, + {'name': 'Санкт-Петербург (Витебский)', 'code': 's9603551'}, + {'name': 'Москва (Казанский вокзал)', 'code': 's9603404'}, + {'name': 'Москва (Ярославский вокзал)', 'code': 's9603408'}, + {'name': 'Москва (Павелецкий вокзал)', 'code': 's9603405'}, + ] + return jsonify(popular_stations) + +@app.route('/api/timetable/station', methods=['GET']) +def get_station_timetable(): + station_id = request.args.get('station') + travel_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d')) + + if not station_id: + return jsonify({'error': 'Station required'}), 400 + + try: + schedule = train_api.get_station_schedule( + station=station_id, + transport_types='suburban', + date=travel_date + ) + return jsonify(schedule) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/timetable/route', methods=['GET']) +def get_route_timetable(): + from_station = request.args.get('from') + to_station = request.args.get('to') + travel_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d')) + + if not from_station or not to_station: + return jsonify({'error': 'Both stations required'}), 400 + + try: + route_data = train_api.get_schedule( + from_station=from_station, + to_station=to_station, + transport_types='suburban', + date=travel_date + ) + return jsonify(route_data) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/health', methods=['GET']) +def health_check(): + return jsonify({'status': 'ok', 'api_configured': bool(API_KEY)}) + +if __name__ == '__main__': + app.run(debug=FLASK_DEBUG, port=PORT) \ No newline at end of file diff --git a/electric-train-tracker/backend/station_cache.py b/electric-train-tracker/backend/station_cache.py new file mode 100644 index 00000000..8ca5871d --- /dev/null +++ b/electric-train-tracker/backend/station_cache.py @@ -0,0 +1,52 @@ +import json +import os +from yaschedule.core import YaSchedule + +def init_api(api_key): + return YaSchedule(api_key) + +def load_all_stations(api_key): + cache_file = 'stations_cache.json' + + if os.path.exists(cache_file): + print("Загрузка из кэша...") + with open(cache_file, 'r', encoding='utf-8') as f: + return json.load(f) + + print("Скачивание с API (первый запуск, подождите)...") + api = init_api(api_key) + stations_data = api.get_all_stations() + + with open(cache_file, 'w', encoding='utf-8') as f: + json.dump(stations_data, f, ensure_ascii=False, indent=2) + + print("Кэш сохранен!") + return stations_data + +def search_stations(stations_data, query): + results = [] + query_lower = query.lower() + + for country in stations_data.get('countries', []): + for region in country.get('regions', []): + for settlement in region.get('settlements', []): + for station in settlement.get('stations', []): + title = station.get('title', '') + station_type = station.get('station_type', '') + + if station_type == 'train_station': + if query_lower in title.lower(): + results.append({ + 'title': title, + 'code': station.get('codes', {}).get('yandex_code'), + 'city': settlement.get('title', '') + }) + + unique = [] + seen = set() + for r in results: + if r['title'] not in seen: + seen.add(r['title']) + unique.append(r) + + return unique[:20] \ No newline at end of file diff --git a/electric-train-tracker/frontend/pages/index.html b/electric-train-tracker/frontend/pages/index.html new file mode 100644 index 00000000..5949d98f --- /dev/null +++ b/electric-train-tracker/frontend/pages/index.html @@ -0,0 +1,102 @@ + + + + + + Электрички — расписание пригородных поездов + + + + + + +
+
+

Электрички

+

Расписание пригородных поездов

+
+ +
+ + + + +
+ +
+
+

Выберите станцию на карте

+

Нажмите в любом месте карты, чтобы найти ближайшие ЖД станции

+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+ +
+
+

Популярные станции

+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+ +
+
+ +
+
+

Любимые станции

+
+

⭐ Нет избранных станций

+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/electric-train-tracker/frontend/static/scripts/app.js b/electric-train-tracker/frontend/static/scripts/app.js new file mode 100644 index 00000000..1c291429 --- /dev/null +++ b/electric-train-tracker/frontend/static/scripts/app.js @@ -0,0 +1,381 @@ +const API_URL = 'http://localhost:5000/api'; + +let selectedStationCode = null; +let selectedStationName = null; +let fromStationCode = null; +let toStationCode = null; +let favorites = []; +let map = null; +let mapMarkers = []; + +$(document).ready(function() { + const saved = localStorage.getItem('railway_favorites'); + if (saved) favorites = JSON.parse(saved); + + const today = new Date().toISOString().split('T')[0]; + $('#station-date').val(today); + $('#route-date').val(today); + + initTabs(); + initSearch(); + loadRecommendedStations(); + renderFavorites(); + + $('#search-station-btn').click(loadStationTimetable); + $('#search-route-btn').click(findRouteTimetable); +}); + +function initTabs() { + $('.tab-btn').click(function() { + $('.tab-btn').removeClass('active'); + $(this).addClass('active'); + + const tab = $(this).data('tab'); + $('.tab-content').removeClass('active'); + $(`#${tab}-tab`).addClass('active'); + $('#results').hide(); + + if (tab === 'map') setTimeout(() => initMap(), 100); + if (tab === 'favorites') renderFavorites(); + }); +} + +function initSearch() { + const fields = [ + { input: '#station-input', suggestions: '#station-suggestions', setCode: (c) => selectedStationCode = c, setName: (n) => selectedStationName = n }, + { input: '#from-input', suggestions: '#from-suggestions', setCode: (c) => fromStationCode = c }, + { input: '#to-input', suggestions: '#to-suggestions', setCode: (c) => toStationCode = c } + ]; + + fields.forEach(field => { + let timeout; + $(field.input).on('input', function() { + clearTimeout(timeout); + const query = $(this).val(); + if (query.length < 2) { + $(field.suggestions).hide(); + return; + } + timeout = setTimeout(() => { + $.get(`${API_URL}/search/stations?q=${encodeURIComponent(query)}`, function(data) { + $(field.suggestions).empty().show(); + data.forEach(s => { + const item = $(`
${s.title}
${s.city || ''}
`); + item.click(() => { + $(field.input).val(s.title); + if (field.setCode) field.setCode(s.code); + if (field.setName) field.setName(s.title); + $(field.suggestions).hide(); + }); + $(field.suggestions).append(item); + }); + }); + }, 300); + }); + $(document).click(e => { + if (!$(e.target).closest(field.input).length) $(field.suggestions).hide(); + }); + }); +} + +function loadRecommendedStations() { + $.get(`${API_URL}/recommended/stations`, function(data) { + const container = $('#popular-list'); + container.empty(); + data.forEach(s => { + const item = $(``); + item.click(() => { + selectedStationCode = s.code; + selectedStationName = s.name; + $('#station-input').val(s.name); + loadStationTimetable(); + }); + container.append(item); + }); + }); +} + +function loadStationTimetable() { + if (!selectedStationCode) { + showMessage('Выберите станцию!'); + return; + } + const date = $('#station-date').val(); + showLoading(); + $('#results').show(); + $.get(`${API_URL}/timetable/station?station=${selectedStationCode}&date=${date}`) + .done(data => displayTimetable(data, selectedStationName)) + .fail(() => $('#schedule-list').html('
Ошибка загрузки
')) + .always(() => hideLoading()); +} + +function findRouteTimetable() { + if (!fromStationCode || !toStationCode) { + showMessage('Выберите обе станции!'); + return; + } + const date = $('#route-date').val(); + showLoading(); + $('#results').show(); + $.get(`${API_URL}/timetable/route?from=${fromStationCode}&to=${toStationCode}&date=${date}`) + .done(data => displayTimetable(data)) + .fail(() => $('#schedule-list').html('
Ошибка загрузки
')) + .always(() => hideLoading()); +} + +function displayTimetable(data, stationContextName) { + let trains = []; + + if (data && data.schedule && Array.isArray(data.schedule)) { + trains = data.schedule; + } else if (data && data.segments && Array.isArray(data.segments)) { + trains = data.segments; + } else if (Array.isArray(data)) { + trains = data; + } + + if (trains.length === 0) { + $('#schedule-list').html('
🚂 Поездов не найдено
'); + $('#results-count').text(''); + return; + } + + $('#results-count').text(`${trains.length} поезд(ов)`); + $('#schedule-list').empty(); + + const favContextName = stationContextName || $('#station-input').val() || 'Станция'; + + trains.forEach(train => { + let trainNumber = '---'; + let depTime = '---'; + let arrTime = '---'; + let days = ''; + let direction = ''; + + if (train.thread && train.thread.number) trainNumber = train.thread.number; + else if (train.number) trainNumber = train.number; + + if (train.departure) depTime = train.departure; + if (train.arrival) arrTime = train.arrival; + if (train.days) days = train.days; + if (train.direction) direction = train.direction; + else if (train.thread && train.thread.title) direction = train.thread.title; + + const fromName = train.from?.title || ''; + const toName = train.to?.title || ''; + + const isFav = favorites.includes(favContextName); + + const card = $(` +
+
+ 🚆 Поезд №${trainNumber} + +
+
+ ${fromName && toName ? `${fromName} → ${toName}` : (direction || favContextName)} +
+
+
+ 🕐 Отправление: + ${depTime} +
+
+ 🏁 Прибытие: + ${arrTime} +
+
+ ${days ? `
📅 ${days}
` : ''} +
+ `); + + card.find('.favorite-btn').click(function(e) { + e.stopPropagation(); + const name = $(this).data('name'); + toggleFavorite(name); + updateFavButton($(this), name); + }); + + $('#schedule-list').append(card); + }); +} + +function toggleFavorite(name) { + const idx = favorites.indexOf(name); + if (idx === -1) { + favorites.push(name); + showMessage(`❤️ ${name} добавлена в избранное`); + } else { + favorites.splice(idx, 1); + showMessage(`💔 ${name} удалена из избранного`); + } + localStorage.setItem('railway_favorites', JSON.stringify(favorites)); + renderFavorites(); +} + +function isFavorite(name) { + return favorites.includes(name); +} + +function updateFavButton(btn, name) { + const fav = isFavorite(name); + btn.toggleClass('active', fav); + btn.find('i').attr('class', `fas ${fav ? 'fa-heart' : 'fa-heart-o'}`); + btn.find('span').text(fav ? 'В избранном' : 'Сохранить'); +} + +function renderFavorites() { + const container = $('#favorites-list'); + if (favorites.length === 0) { + container.html('

⭐ Нет избранных станций. Добавьте их из расписания!

'); + return; + } + container.empty(); + favorites.forEach(name => { + const card = $(` +
+
${name}
+ +
+ `); + card.click(e => { + if (!$(e.target).hasClass('remove-fav')) { + selectedStationName = name; + $('#station-input').val(name); + $('.tab-btn[data-tab="station"]').click(); + findStationCodeByName(name); + } + }); + card.find('.remove-fav').click(e => { + e.stopPropagation(); + toggleFavorite(name); + }); + container.append(card); + }); +} + +function findStationCodeByName(stationName) { + $.get(`${API_URL}/search/stations?q=${encodeURIComponent(stationName)}`, function(data) { + if (data && data.length > 0) { + selectedStationCode = data[0].code; + selectedStationName = data[0].title; + loadStationTimetable(); + } else { + showMessage(`❌ Станция "${stationName}" не найдена`); + } + }); +} + +function initMap() { + if (map) return; + if (typeof ymaps === 'undefined') { setTimeout(initMap, 200); return; } + + ymaps.ready(() => { + map = new ymaps.Map('map', { + center: [55.751574, 37.573856], + zoom: 10, + controls: ['zoomControl', 'fullscreenControl', 'geolocationControl'] + }); + + const searchControl = new ymaps.control.SearchControl({ options: { provider: 'yandex#search', size: 'large', noSelect: true } }); + map.controls.add(searchControl); + + map.events.add('click', function(e) { + const coords = e.get('coords'); + findNearbyStations(coords[0], coords[1]); + }); + + $('#find-my-location').click(function() { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(position) { + const lat = position.coords.latitude; + const lng = position.coords.longitude; + map.setCenter([lat, lng], 14); + findNearbyStations(lat, lng); + showMessage(`📍 Определено ваше местоположение`); + }, function() { showMessage(`❌ Не удалось определить местоположение`); }); + } else { showMessage(`❌ Геолокация не поддерживается`); } + }); + }); +} + +function findNearbyStations(lat, lng) { + showMessage(`🔍 Поиск станций рядом...`); + $('#nearby-stations-list').html('
Загрузка...
'); + $('#selected-station-info').show(); + + $.get(`${API_URL}/geo/nearby?lat=${lat}&lng=${lng}&distance=50`) + .done(data => displayNearbyStations(data)) + .fail(() => $('#nearby-stations-list').html('
Ошибка поиска станций
')); +} + +function displayNearbyStations(stations) { + if (!stations || stations.length === 0) { + $('#nearby-stations-list').html('
🚉 ЖД станции не найдены
'); + return; + } + + $('#nearby-stations-list').empty(); + mapMarkers.forEach(marker => map.geoObjects.remove(marker)); + mapMarkers = []; + + stations.forEach(station => { + const stationLat = station.latitude; + const stationLng = station.longitude; + const stationName = station.title; + const stationCode = station.code; + const distance = station.distance ? station.distance.toFixed(1) : '?'; + const city = station.city || ''; + const buttonId = 'btn_' + Math.random().toString(36).substr(2, 9); + + if (stationLat && stationLng) { + const marker = new ymaps.Placemark([stationLat, stationLng], { + hintContent: stationName, + balloonContent: `
${stationName}
Расстояние: ${distance} км
` + }, { preset: 'islands#blueRailwayIcon' }); + + marker.events.add('balloonopen', function() { + const button = document.getElementById(buttonId); + if (button) button.onclick = () => { selectStationFromMap(stationCode, stationName); marker.balloon.close(); }; + }); + + map.geoObjects.add(marker); + mapMarkers.push(marker); + } + + const stationItem = $(`
${stationName}${distance} км${city ? `${city}` : ''}
`); + stationItem.find('.select-station-btn').click(() => selectStationFromMap(stationCode, stationName)); + stationItem.click(() => selectStationFromMap(stationCode, stationName)); + $('#nearby-stations-list').append(stationItem); + }); +} + +function selectStationFromMap(code, name) { + if (code) selectedStationCode = code; + selectedStationName = name; + $('#station-input').val(name); + showMessage(`✅ Выбрана станция: ${name}`); + $('.tab-btn[data-tab="station"]').click(); + if (code) setTimeout(() => loadStationTimetable(), 100); +} + +window.selectStationFromMap = selectStationFromMap; + +function showMessage(text) { + let toast = $('#toast'); + if (!toast.length) { + $('body').append(''); + toast = $('#toast'); + } + toast.text(text).fadeIn(200); + setTimeout(() => toast.fadeOut(500), 2000); +} + +function showLoading() { + $('#schedule-list').html('
Загрузка расписания...
'); +} + +function hideLoading() {} \ No newline at end of file diff --git a/electric-train-tracker/frontend/static/styles/main.css b/electric-train-tracker/frontend/static/styles/main.css new file mode 100644 index 00000000..6f3c6916 --- /dev/null +++ b/electric-train-tracker/frontend/static/styles/main.css @@ -0,0 +1,692 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', 'Poppins', -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: #2d3436; +} + +.app-container { + max-width: 600px; + margin: 0 auto; + padding: 16px; + min-height: 100vh; +} + +/* Новый стиль хедера - градиент розово-оранжевый */ +.header { + text-align: center; + margin-bottom: 20px; + padding: 24px 20px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + border-radius: 32px; + color: white; + box-shadow: 0 10px 30px rgba(0,0,0,0.2); + position: relative; + overflow: hidden; +} + +.header::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); + animation: rotate 20s linear infinite; +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.header h1 { + font-size: 28px; + font-weight: 700; + letter-spacing: -0.5px; + position: relative; + z-index: 1; +} + +.header h1 i { + margin-right: 10px; + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-5px); } +} + +.subtitle { + font-size: 13px; + opacity: 0.9; + margin-top: 6px; + position: relative; + z-index: 1; +} + +/* Новые вкладки - стеклянный эффект */ +.tabs { + display: flex; + gap: 8px; + margin-bottom: 20px; + background: rgba(255,255,255,0.2); + backdrop-filter: blur(10px); + padding: 6px; + border-radius: 60px; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.tab-btn { + flex: 1; + padding: 12px; + border: none; + background: transparent; + border-radius: 50px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + color: rgba(255,255,255,0.8); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backdrop-filter: blur(5px); +} + +.tab-btn.active { + background: white; + color: #f5576c; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + transform: scale(1.02); +} + +.tab-btn:hover:not(.active) { + background: rgba(255,255,255,0.3); + color: white; +} + +.tab-content { + display: none; + animation: slideUp 0.4s ease-out; +} + +.tab-content.active { + display: block; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Карточки - новый дизайн */ +.card { + background: rgba(255,255,255,0.95); + backdrop-filter: blur(10px); + border-radius: 28px; + padding: 24px; + margin-bottom: 20px; + box-shadow: 0 8px 32px rgba(0,0,0,0.1); + border: 1px solid rgba(255,255,255,0.3); + transition: transform 0.2s, box-shadow 0.2s; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(0,0,0,0.15); +} + +.card h3 { + font-size: 18px; + margin-bottom: 16px; + color: #f5576c; + display: flex; + align-items: center; + gap: 8px; +} + +.input-group { + margin-bottom: 18px; + position: relative; +} + +.input-group label { + display: block; + font-size: 13px; + font-weight: 600; + margin-bottom: 8px; + color: #555; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.input-group label i { + margin-right: 6px; + color: #f5576c; +} + +.input-group input { + width: 100%; + padding: 14px 16px; + border: 2px solid #e0e0e0; + border-radius: 20px; + font-size: 16px; + transition: all 0.3s; + background: white; +} + +.input-group input:focus { + outline: none; + border-color: #f5576c; + box-shadow: 0 0 0 3px rgba(245,87,108,0.2); +} + +/* Кнопки - градиентные */ +.btn-primary { + width: 100%; + padding: 14px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + border: none; + border-radius: 30px; + font-size: 16px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s; + text-transform: uppercase; + letter-spacing: 1px; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(245,87,108,0.4); +} + +.btn-primary:active { + transform: translateY(0); +} + +/* Кнопки карты */ +.map-buttons { + display: flex; + gap: 12px; + margin-bottom: 15px; +} + +.map-btn { + flex: 1; + padding: 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 30px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + transition: all 0.3s; +} + +.map-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(102,126,234,0.4); +} + +/* Карта */ +.map-container { + width: 100%; + height: 400px; + border-radius: 24px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + border: 3px solid white; +} + +/* Популярные станции - чипсы */ +.popular-list { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +.popular-item { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + padding: 8px 18px; + border-radius: 40px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + color: white; + box-shadow: 0 2px 8px rgba(245,87,108,0.3); +} + +.popular-item:hover { + transform: scale(1.05); + box-shadow: 0 4px 15px rgba(245,87,108,0.5); +} + +/* Подсказки при поиске */ +.suggestions { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border-radius: 20px; + box-shadow: 0 8px 25px rgba(0,0,0,0.15); + z-index: 100; + max-height: 220px; + overflow-y: auto; + margin-top: 5px; +} + +.suggestion-item { + padding: 14px 16px; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + transition: background 0.2s; +} + +.suggestion-item:hover { + background: linear-gradient(135deg, #f093fb20 0%, #f5576c20 100%); +} + +.suggestion-item .station-name { + font-weight: 600; + color: #333; +} + +.suggestion-item .station-loc { + font-size: 11px; + color: #f5576c; + margin-top: 3px; +} + +/* Результаты */ +.results-container { + background: rgba(255,255,255,0.95); + backdrop-filter: blur(10px); + border-radius: 28px; + padding: 20px; + margin-top: 20px; + border: 1px solid rgba(255,255,255,0.3); +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 2px solid #f0f0f0; +} + +.results-header h3 { + color: #f5576c; + font-size: 16px; +} + +#results-count { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + padding: 4px 12px; + border-radius: 30px; + color: white; + font-size: 12px; + font-weight: bold; +} + +/* Карточки поездов */ +.train-card { + background: white; + border-radius: 20px; + padding: 16px; + margin-bottom: 12px; + transition: all 0.3s; + border: 1px solid #f0f0f0; + position: relative; + overflow: hidden; +} + +.train-card::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.train-card:hover { + transform: translateX(5px); + box-shadow: 0 8px 25px rgba(0,0,0,0.1); +} + +.train-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.train-number { + font-weight: 800; + font-size: 16px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.train-route { + font-size: 13px; + color: #666; + margin-bottom: 12px; + font-weight: 500; +} + +.train-time { + display: flex; + justify-content: space-between; + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed #e0e0e0; +} + +.departure-info, .arrival-info { + flex: 1; +} + +.time-label { + font-size: 11px; + color: #999; + display: block; +} + +.time-value { + font-size: 20px; + font-weight: 800; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.train-days { + margin-top: 10px; + font-size: 11px; + color: #f5576c; + text-align: center; + font-weight: 500; +} + +/* Кнопка избранного */ +.favorite-btn { + background: rgba(245,87,108,0.1); + border: none; + cursor: pointer; + color: #f5576c; + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 30px; + font-size: 12px; + font-weight: 600; + transition: all 0.3s; +} + +.favorite-btn.active { + background: #f5576c; + color: white; +} + +.favorite-btn.active i { + color: white; +} + +.favorite-btn:hover { + transform: scale(1.05); +} + +/* Список станций рядом */ +.nearby-station-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + margin-bottom: 10px; + background: white; + border-radius: 20px; + cursor: pointer; + transition: all 0.3s; + border: 1px solid #f0f0f0; +} + +.nearby-station-item:hover { + background: linear-gradient(135deg, #f093fb10 0%, #f5576c10 100%); + transform: translateX(5px); +} + +.nearby-station-item i { + color: #f5576c; + font-size: 20px; + margin-right: 12px; +} + +.nearby-station-item .station-info { + flex: 1; +} + +.nearby-station-item .station-info strong { + display: block; + font-size: 14px; + color: #333; +} + +.nearby-station-item .distance { + font-size: 11px; + color: #f5576c; + font-weight: 500; +} + +.nearby-station-item .city { + font-size: 11px; + color: #999; +} + +.select-station-btn { + padding: 6px 16px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + border: none; + border-radius: 30px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + transition: all 0.3s; +} + +.select-station-btn:hover { + transform: scale(1.05); +} + +/* Избранное */ +.favorite-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 18px; + background: white; + border-radius: 20px; + margin-bottom: 10px; + cursor: pointer; + transition: all 0.3s; + border: 1px solid #f0f0f0; +} + +.favorite-card:hover { + background: linear-gradient(135deg, #f093fb10 0%, #f5576c10 100%); + transform: translateX(5px); +} + +.favorite-card i { + color: #f5576c; +} + +.remove-fav { + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%); + color: white; + border: none; + padding: 6px 14px; + border-radius: 30px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + transition: all 0.3s; +} + +.remove-fav:hover { + transform: scale(1.05); +} + +/* Уведомления */ +#toast { + background: linear-gradient(135deg, #2d3436 0%, #1a1a2e 100%); + color: white; + padding: 14px 24px; + border-radius: 50px; + font-size: 14px; + font-weight: 500; + box-shadow: 0 8px 25px rgba(0,0,0,0.2); + border: 1px solid rgba(255,255,255,0.2); +} + +/* Загрузчик */ +.loader i { + font-size: 32px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Пустые состояния */ +.empty-message { + text-align: center; + color: #999; + padding: 40px; + font-size: 14px; +} + +/* Дата инпут */ +.date-input { + width: 100%; + padding: 14px 16px; + border: 2px solid #e0e0e0; + border-radius: 20px; + font-size: 16px; + transition: all 0.3s; + background: white; +} + +.date-input:focus { + outline: none; + border-color: #f5576c; + box-shadow: 0 0 0 3px rgba(245,87,108,0.2); +} + +/* Адаптив для мобильных */ +@media (max-width: 480px) { + .app-container { + padding: 12px; + } + + .header h1 { + font-size: 22px; + } + + .tab-btn { + padding: 10px; + font-size: 11px; + } + + .card { + padding: 18px; + } + + .train-time { + flex-direction: column; + gap: 10px; + } + + .arrival-info { + text-align: left; + } + + .time-value { + font-size: 18px; + } + + .nearby-station-item { + flex-wrap: wrap; + } + + .select-station-btn { + margin-top: 8px; + width: 100%; + } +} + +/* Скроллбар */ +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + border-radius: 10px; +} + +/* Анимация для появления */ +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.loading-small { + text-align: center; + padding: 20px; + color: #f5576c; + animation: pulse 1.5s ease-in-out infinite; +} + +.error-small { + text-align: center; + padding: 20px; + color: #ff6b6b; +} \ No newline at end of file