diff --git a/static/css/trips-calendar.css b/static/css/trips-calendar.css new file mode 100644 index 0000000..81c5e3d --- /dev/null +++ b/static/css/trips-calendar.css @@ -0,0 +1,159 @@ +/** + * Стили для календаря поездок (карточки из upcoming-trips.json) + * Заменяет внешний Tockify виджет + */ + +.trips-calendar { + margin: 20px 0; + padding: 0; +} + +.trips-calendar .trip-card { + display: flex; + flex-direction: column; + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 12px; + margin-bottom: 20px; + overflow: hidden; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.trips-calendar .trip-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +.trips-calendar .trip-image { + position: relative; + width: 100%; + height: 200px; + overflow: hidden; +} + +.trips-calendar .trip-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.trips-calendar .trip-card:hover .trip-image img { + transform: scale(1.05); +} + +.trips-calendar .trip-overlay { + position: absolute; + top: 16px; + right: 16px; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 8px 12px; + border-radius: 6px; + font-size: 0.9em; + font-weight: 500; +} + +.trips-calendar .trip-content { + padding: 20px; +} + +.trips-calendar .trip-content h3 { + margin: 0 0 12px 0; + font-size: 1.4em; + font-weight: 600; + color: #333; + line-height: 1.3; +} + +.trips-calendar .trip-details p { + margin: 0 0 16px 0; + color: #666; + line-height: 1.5; + font-size: 0.95em; +} + +.trips-calendar .trip-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.trips-calendar .trip-meta span { + background: #f0f4f8; + color: #2d3748; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.85em; + border: 1px solid #e2e8f0; +} + +/* Адаптивность */ +@media (min-width: 768px) { + .trips-calendar { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 24px; + } + + .trips-calendar .trip-card { + margin-bottom: 0; + } +} + +@media (min-width: 1024px) { + .trips-calendar { + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + } +} + +/* Состояние загрузки */ +.trips-calendar .loading { + text-align: center; + padding: 40px 20px; + color: #666; + font-size: 1.1em; +} + +.trips-calendar .error { + text-align: center; + padding: 40px 20px; + color: #e53e3e; + background: #fed7d7; + border-radius: 8px; + margin: 20px 0; +} + +.trips-calendar .no-trips { + text-align: center; + padding: 40px 20px; + color: #666; + background: #f7fafc; + border-radius: 8px; + border: 1px dashed #cbd5e0; +} + +/* Темная тема (если используется) */ +@media (prefers-color-scheme: dark) { + .trips-calendar .trip-card { + background: #2d3748; + border-color: #4a5568; + color: #e2e8f0; + } + + .trips-calendar .trip-content h3 { + color: #f7fafc; + } + + .trips-calendar .trip-details p { + color: #a0aec0; + } + + .trips-calendar .trip-meta span { + background: #4a5568; + color: #e2e8f0; + border-color: #718096; + } +} \ No newline at end of file diff --git a/static/js/trip-form-loader.js b/static/js/trip-form-loader.js new file mode 100644 index 0000000..8c4820e --- /dev/null +++ b/static/js/trip-form-loader.js @@ -0,0 +1,110 @@ +/** + * Динамическое заполнение dropdown формы поездками из upcoming-trips.json + * Синхронизировано с Telegram Bot управлением + */ + +class TripFormLoader { + constructor(selectId = 'trip_period', jsonPath = '/data/upcoming-trips.json') { + this.selectId = selectId; + this.jsonPath = jsonPath; + } + + async loadTripsData() { + try { + const response = await fetch(this.jsonPath); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Ошибка загрузки данных поездок для формы:', error); + return null; + } + } + + async populateTripsDropdown() { + console.log(`🔍 Ищем dropdown с ID: "${this.selectId}"`); + const select = document.getElementById(this.selectId); + if (!select) { + console.error(`❌ Dropdown с ID "${this.selectId}" не найден`); + console.log('📋 Доступные элементы с ID:', Array.from(document.querySelectorAll('[id]')).map(el => el.id)); + return; + } + + console.log(`✅ Dropdown найден, загружаем данные из: ${this.jsonPath}`); + const data = await this.loadTripsData(); + if (!data || !data.trips) { + console.error('❌ Не удалось загрузить данные поездок'); + return; + } + + console.log(`📊 Данные загружены: ${data.trips.length} поездок всего`); + + // Фильтруем только активные поездки и сортируем по порядку + const activeTrips = data.trips + .filter(trip => trip.active === true) + .sort((a, b) => (a.order || 0) - (b.order || 0)); + + // Очищаем существующие опции (кроме первой пустой) + const firstOption = select.querySelector('option[value=""]'); + select.innerHTML = ''; + + // Возвращаем первую пустую опцию + if (firstOption) { + select.appendChild(firstOption); + } else { + const emptyOption = document.createElement('option'); + emptyOption.value = ''; + emptyOption.textContent = ''; + select.appendChild(emptyOption); + } + + // Добавляем активные поездки + activeTrips.forEach(trip => { + const option = document.createElement('option'); + option.value = trip.title; + option.textContent = trip.title; + select.appendChild(option); + }); + + // Добавляем статичную опцию "Свой вариант без БВС" + const customOption = document.createElement('option'); + customOption.value = 'Свой вариант без БВС'; + customOption.textContent = 'Свой вариант без БВС'; + select.appendChild(customOption); + + console.log(`✅ Dropdown формы заполнен: ${activeTrips.length} активных поездок`); + } + + // Метод для обновления (можно вызывать для перезагрузки) + async refresh() { + await this.populateTripsDropdown(); + } +} + +// Глобальная переменная для доступа к загрузчику +let tripFormLoader; + +// Инициализация при загрузке DOM +document.addEventListener('DOMContentLoaded', function() { + // Проверяем, есть ли на странице dropdown для поездок + const tripSelect = document.getElementById('trip_period'); + if (tripSelect) { + tripFormLoader = new TripFormLoader(); + tripFormLoader.populateTripsDropdown(); + + console.log('🗓️ Загрузчик поездок для формы инициализирован'); + } +}); + +// Функция для обновления dropdown (можно вызывать извне) +function refreshTripFormDropdown() { + if (tripFormLoader) { + tripFormLoader.refresh(); + } +} + +// Экспорт для использования в других скриптах +if (typeof module !== 'undefined' && module.exports) { + module.exports = TripFormLoader; +} \ No newline at end of file diff --git a/static/js/upcoming-trips.js b/static/js/upcoming-trips.js new file mode 100644 index 0000000..3880d11 --- /dev/null +++ b/static/js/upcoming-trips.js @@ -0,0 +1,130 @@ +/** + * Динамическая загрузка карточек предстоящих поездок + * Загружает данные из JSON файла и генерирует HTML карточки + */ + +class UpcomingTripsLoader { + constructor(containerId = 'trips-grid', jsonPath = '/data/upcoming-trips.json') { + this.containerId = containerId; + this.jsonPath = jsonPath; + this.tripsData = null; + } + + async loadTripsData() { + try { + const response = await fetch(this.jsonPath); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + this.tripsData = await response.json(); + return this.tripsData; + } catch (error) { + console.error('Ошибка загрузки данных поездок:', error); + return null; + } + } + + generateTripCard(trip) { + const metaHtml = trip.meta ? trip.meta.map(meta => + `${meta}` + ).join('') : ''; + + return ` +
+
+ ${trip.title} +
+ ${trip.period} +
+
+
+

${trip.title}

+
+

${trip.description}

+
+ ${metaHtml} +
+
+ +
+
+ `; + } + + async renderTrips() { + const container = document.getElementById(this.containerId); + if (!container) { + console.error(`Контейнер с ID "${this.containerId}" не найден`); + return; + } + + // Показываем индикатор загрузки + container.innerHTML = '
Загрузка поездок...
'; + + const data = await this.loadTripsData(); + if (!data || !data.trips) { + container.innerHTML = '
Ошибка загрузки данных поездок
'; + return; + } + + // Фильтруем только активные поездки и сортируем по порядку + const activeTrips = data.trips + .filter(trip => trip.active === true) + .sort((a, b) => (a.order || 0) - (b.order || 0)); + + if (activeTrips.length === 0) { + container.innerHTML = '
Нет активных поездок
'; + return; + } + + // Генерируем HTML для всех активных поездок + const tripsHtml = activeTrips.map(trip => this.generateTripCard(trip)).join(''); + container.innerHTML = tripsHtml; + + console.log(`✅ Загружено ${activeTrips.length} активных поездок`); + } + + // Метод для обновления данных (можно вызывать для перезагрузки) + async refresh() { + await this.renderTrips(); + } + + // Получить данные о конкретной поездке + getTripById(id) { + if (!this.tripsData || !this.tripsData.trips) return null; + return this.tripsData.trips.find(trip => trip.id === id); + } + + // Получить все активные поездки + getActiveTrips() { + if (!this.tripsData || !this.tripsData.trips) return []; + return this.tripsData.trips.filter(trip => trip.active === true); + } +} + +// Глобальная переменная для доступа к загрузчику +let upcomingTripsLoader; + +// Инициализация при загрузке DOM +document.addEventListener('DOMContentLoaded', function() { + // Проверяем, есть ли на странице контейнер для поездок + const tripsContainer = document.getElementById('trips-grid'); + if (tripsContainer) { + upcomingTripsLoader = new UpcomingTripsLoader(); + upcomingTripsLoader.renderTrips(); + + console.log('🗓️ Загрузчик предстоящих поездок инициализирован'); + } +}); + +// Функция для обновления поездок (можно вызывать извне) +function refreshUpcomingTrips() { + if (upcomingTripsLoader) { + upcomingTripsLoader.refresh(); + } +} + +// Экспорт для использования в других скриптах +if (typeof module !== 'undefined' && module.exports) { + module.exports = UpcomingTripsLoader; +} \ No newline at end of file