Исправление путей к статическим файлам
- Перенос trips-calendar.css в /static/css/ - Перенос upcoming-trips.js и trip-form-loader.js в /static/js/ - Удаление файлов из неправильной папки /public/
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 =>
|
||||||
|
`<span>${meta}</span>`
|
||||||
|
).join('') : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="trip-card" data-value="${trip.title}">
|
||||||
|
<div class="trip-image">
|
||||||
|
<img src="${trip.image}" alt="${trip.title}" loading="lazy">
|
||||||
|
<div class="trip-overlay">
|
||||||
|
<span class="trip-period">${trip.period}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trip-content">
|
||||||
|
<h3>${trip.title}</h3>
|
||||||
|
<div class="trip-details">
|
||||||
|
<p>${trip.description}</p>
|
||||||
|
<div class="trip-meta">
|
||||||
|
${metaHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Кнопка выбора поездки убрана - dropdown заполняется автоматически -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderTrips() {
|
||||||
|
const container = document.getElementById(this.containerId);
|
||||||
|
if (!container) {
|
||||||
|
console.error(`Контейнер с ID "${this.containerId}" не найден`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем индикатор загрузки
|
||||||
|
container.innerHTML = '<div class="loading">Загрузка поездок...</div>';
|
||||||
|
|
||||||
|
const data = await this.loadTripsData();
|
||||||
|
if (!data || !data.trips) {
|
||||||
|
container.innerHTML = '<div class="error">Ошибка загрузки данных поездок</div>';
|
||||||
|
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 = '<div class="no-trips">Нет активных поездок</div>';
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user