- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
588 lines
24 KiB
JavaScript
588 lines
24 KiB
JavaScript
/**
|
||
* Korea Tourism Agency - Main JavaScript
|
||
* Основные интерактивные функции для сайта
|
||
*/
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
|
||
// ==========================================
|
||
// Инициализация AOS (Animate On Scroll)
|
||
// ==========================================
|
||
if (typeof AOS !== 'undefined') {
|
||
AOS.init({
|
||
duration: 800,
|
||
easing: 'ease-in-out',
|
||
once: true,
|
||
offset: 100
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Навигация и мобильное меню
|
||
// ==========================================
|
||
const navbar = document.querySelector('.navbar');
|
||
|
||
// Добавление класса при скролле
|
||
window.addEventListener('scroll', function() {
|
||
if (window.scrollY > 100) {
|
||
if (navbar) navbar.classList.add('scrolled');
|
||
} else {
|
||
if (navbar) navbar.classList.remove('scrolled');
|
||
}
|
||
});
|
||
|
||
// ==========================================
|
||
// Инициализация компонентов бронирования
|
||
// ==========================================
|
||
|
||
// Компонент для проверки доступности на главной странице
|
||
const availabilityContainer = document.getElementById('availability-checker-container');
|
||
const guideSelectorContainer = document.getElementById('guide-selector-container');
|
||
|
||
if (availabilityContainer) {
|
||
const availabilityChecker = new AvailabilityChecker({
|
||
container: availabilityContainer,
|
||
mode: 'detailed',
|
||
showSuggestions: true,
|
||
onAvailabilityCheck: function(result) {
|
||
if (result.availableGuides && result.availableGuides.length > 0) {
|
||
// Показать селектор гидов если есть доступные
|
||
if (guideSelectorContainer) {
|
||
guideSelectorContainer.style.display = 'block';
|
||
|
||
const guideSelector = new GuideSelector({
|
||
container: guideSelectorContainer,
|
||
mode: 'booking',
|
||
showAvailability: true,
|
||
selectedDate: result.date,
|
||
onGuideSelect: function(guide) {
|
||
// Перейти к бронированию с выбранным гидом
|
||
window.location.href = `/routes?guide=${guide.id}&date=${result.date}`;
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
if (guideSelectorContainer) {
|
||
guideSelectorContainer.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Календарь гидов на странице гидов
|
||
const guidesCalendarContainer = document.getElementById('guides-calendar-container');
|
||
if (guidesCalendarContainer) {
|
||
const guidesCalendar = new GuideCalendarWidget({
|
||
container: guidesCalendarContainer,
|
||
mode: 'readonly',
|
||
showControls: false,
|
||
showGuideInfo: true
|
||
});
|
||
}
|
||
|
||
// Компоненты бронирования на странице маршрута
|
||
const bookingAvailabilityContainer = document.getElementById('booking-availability-checker');
|
||
const bookingGuideSelectorContainer = document.getElementById('booking-guide-selector');
|
||
|
||
if (bookingAvailabilityContainer) {
|
||
const bookingAvailabilityChecker = new AvailabilityChecker({
|
||
container: bookingAvailabilityContainer,
|
||
mode: 'inline',
|
||
showSuggestions: false,
|
||
onAvailabilityCheck: function(result) {
|
||
if (result.availableGuides && result.availableGuides.length > 0) {
|
||
if (bookingGuideSelectorContainer) {
|
||
bookingGuideSelectorContainer.style.display = 'block';
|
||
|
||
const bookingGuideSelector = new GuideSelector({
|
||
container: bookingGuideSelectorContainer,
|
||
mode: 'booking',
|
||
showAvailability: false,
|
||
availableGuides: result.availableGuides,
|
||
onGuideSelect: function(guide) {
|
||
// Заполнить скрытое поле с ID гида
|
||
const selectedGuideIdInput = document.getElementById('selectedGuideId');
|
||
const preferredDateInput = document.getElementById('preferred_date');
|
||
const submitBtn = document.getElementById('submitBookingBtn');
|
||
|
||
if (selectedGuideIdInput) {
|
||
selectedGuideIdInput.value = guide.id;
|
||
}
|
||
|
||
if (preferredDateInput) {
|
||
preferredDateInput.value = result.date;
|
||
}
|
||
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Поиск по сайту (обновленная версия)
|
||
// ==========================================
|
||
const searchInput = document.getElementById('search-input');
|
||
const searchResults = document.getElementById('search-results');
|
||
|
||
if (searchInput && searchResults) {
|
||
let searchTimeout;
|
||
|
||
searchInput.addEventListener('input', function() {
|
||
const query = this.value.trim();
|
||
|
||
clearTimeout(searchTimeout);
|
||
|
||
if (query.length < 2) {
|
||
searchResults.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
searchTimeout = setTimeout(() => {
|
||
performSearch(query);
|
||
}, 300);
|
||
});
|
||
|
||
// Скрытие результатов при клике вне поиска
|
||
document.addEventListener('click', function(e) {
|
||
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
|
||
searchResults.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
async function performSearch(query) {
|
||
try {
|
||
const response = await fetch('/api/search?q=' + encodeURIComponent(query));
|
||
const data = await response.json();
|
||
|
||
displaySearchResults(data);
|
||
} catch (error) {
|
||
console.error('Search error:', error);
|
||
}
|
||
}
|
||
|
||
function displaySearchResults(results) {
|
||
if (!searchResults) return;
|
||
|
||
let html = '';
|
||
|
||
if (results.routes && results.routes.length > 0) {
|
||
html += '<div class="search-category"><h6>투어</h6>';
|
||
results.routes.forEach(route => {
|
||
html += '<div class="search-item">';
|
||
html += '<a href="/routes/' + route.id + '">';
|
||
html += '<strong>' + (route.name_ko || route.name_en) + '</strong>';
|
||
html += '<small class="text-muted d-block">' + route.location + '</small>';
|
||
html += '</a></div>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
if (results.guides && results.guides.length > 0) {
|
||
html += '<div class="search-category"><h6>가이드</h6>';
|
||
results.guides.forEach(guide => {
|
||
html += '<div class="search-item">';
|
||
html += '<a href="/guides/' + guide.id + '">';
|
||
html += '<strong>' + guide.name + '</strong>';
|
||
html += '<small class="text-muted d-block">' + guide.specialization + '</small>';
|
||
html += '</a></div>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
if (results.articles && results.articles.length > 0) {
|
||
html += '<div class="search-category"><h6>기사</h6>';
|
||
results.articles.forEach(article => {
|
||
html += '<div class="search-item">';
|
||
html += '<a href="/articles/' + article.id + '">';
|
||
html += '<strong>' + (article.title_ko || article.title_en) + '</strong>';
|
||
html += '</a></div>';
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
if (!html) {
|
||
html = '<div class="search-item text-muted">검색 결과가 없습니다</div>';
|
||
}
|
||
|
||
searchResults.innerHTML = html;
|
||
searchResults.style.display = 'block';
|
||
}
|
||
|
||
// ==========================================
|
||
// Фильтрация туров
|
||
// ==========================================
|
||
const routeFilters = document.querySelectorAll('.route-filter');
|
||
const routeCards = document.querySelectorAll('.route-card');
|
||
|
||
routeFilters.forEach(filter => {
|
||
filter.addEventListener('click', function() {
|
||
const category = this.dataset.category;
|
||
|
||
// Обновление активного фильтра
|
||
routeFilters.forEach(f => f.classList.remove('active'));
|
||
this.classList.add('active');
|
||
|
||
// Фильтрация карточек
|
||
routeCards.forEach(card => {
|
||
if (category === 'all' || card.dataset.category === category) {
|
||
card.style.display = 'block';
|
||
card.classList.add('fade-in');
|
||
} else {
|
||
card.style.display = 'none';
|
||
card.classList.remove('fade-in');
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// Форма бронирования
|
||
// ==========================================
|
||
const bookingForm = document.getElementById('booking-form');
|
||
|
||
if (bookingForm) {
|
||
bookingForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const submitBtn = this.querySelector('button[type="submit"]');
|
||
const originalText = submitBtn.textContent;
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 전송 중...';
|
||
|
||
try {
|
||
const formData = new FormData(this);
|
||
const response = await fetch('/api/booking', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
showAlert('success', '예약 요청이 성공적으로 전송되었습니다!');
|
||
this.reset();
|
||
} else {
|
||
showAlert('danger', result.error || '오류가 발생했습니다.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Booking error:', error);
|
||
showAlert('danger', '네트워크 오류가 발생했습니다.');
|
||
} finally {
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = originalText;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Форма контактов
|
||
// ==========================================
|
||
const contactForm = document.getElementById('contact-form');
|
||
|
||
if (contactForm) {
|
||
contactForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const submitBtn = this.querySelector('button[type="submit"]');
|
||
const originalText = submitBtn.textContent;
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 전송 중...';
|
||
|
||
try {
|
||
const formData = new FormData(this);
|
||
const response = await fetch('/api/contact', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
showAlert('success', '메시지가 성공적으로 전송되었습니다!');
|
||
this.reset();
|
||
} else {
|
||
showAlert('danger', result.error || '오류가 발생했습니다.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Contact error:', error);
|
||
showAlert('danger', '네트워크 오류가 발생했습니다.');
|
||
} finally {
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = originalText;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Галерея изображений
|
||
// ==========================================
|
||
const galleryItems = document.querySelectorAll('.gallery-item');
|
||
|
||
galleryItems.forEach(item => {
|
||
item.addEventListener('click', function() {
|
||
const img = this.querySelector('img');
|
||
if (img) {
|
||
showImageModal(img.src, img.alt || 'Gallery Image');
|
||
}
|
||
});
|
||
});
|
||
|
||
function showImageModal(src, alt) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'image-modal';
|
||
modal.innerHTML =
|
||
'<div class="image-modal-backdrop">' +
|
||
'<div class="image-modal-content">' +
|
||
'<button class="image-modal-close" type="button">' +
|
||
'<i class="fas fa-times"></i>' +
|
||
'</button>' +
|
||
'<img src="' + src + '" alt="' + alt + '" class="img-fluid">' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
document.body.appendChild(modal);
|
||
setTimeout(() => modal.classList.add('show'), 10);
|
||
|
||
// Закрытие модального окна
|
||
const closeModal = () => {
|
||
modal.classList.remove('show');
|
||
setTimeout(() => {
|
||
if (modal.parentNode) {
|
||
document.body.removeChild(modal);
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
modal.querySelector('.image-modal-close').addEventListener('click', closeModal);
|
||
modal.querySelector('.image-modal-backdrop').addEventListener('click', function(e) {
|
||
if (e.target === this) closeModal();
|
||
});
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') closeModal();
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Плавная прокрутка к секциям
|
||
// ==========================================
|
||
const scrollLinks = document.querySelectorAll('a[href^="#"]');
|
||
|
||
scrollLinks.forEach(link => {
|
||
link.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
|
||
const targetId = this.getAttribute('href').substring(1);
|
||
const targetElement = document.getElementById(targetId);
|
||
|
||
if (targetElement) {
|
||
targetElement.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'start'
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// Валидация форм
|
||
// ==========================================
|
||
const forms = document.querySelectorAll('.needs-validation');
|
||
|
||
forms.forEach(form => {
|
||
form.addEventListener('submit', function(e) {
|
||
if (!form.checkValidity()) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
form.classList.add('was-validated');
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// Tooltips и Popovers
|
||
// ==========================================
|
||
if (typeof bootstrap !== 'undefined') {
|
||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||
});
|
||
|
||
const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||
return new bootstrap.Popover(popoverTriggerEl);
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Lazy loading изображений
|
||
// ==========================================
|
||
const lazyImages = document.querySelectorAll('img[data-src]');
|
||
|
||
if ('IntersectionObserver' in window) {
|
||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
const img = entry.target;
|
||
img.src = img.dataset.src;
|
||
img.classList.remove('lazy');
|
||
imageObserver.unobserve(img);
|
||
}
|
||
});
|
||
});
|
||
|
||
lazyImages.forEach(img => imageObserver.observe(img));
|
||
} else {
|
||
// Fallback для старых браузеров
|
||
lazyImages.forEach(img => {
|
||
img.src = img.dataset.src;
|
||
img.classList.remove('lazy');
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// Утилитарные функции
|
||
// ==========================================
|
||
function showAlert(type, message) {
|
||
const alertContainer = document.getElementById('alert-container') || createAlertContainer();
|
||
|
||
const alert = document.createElement('div');
|
||
alert.className = 'alert alert-' + type + ' alert-dismissible fade show';
|
||
alert.innerHTML =
|
||
message +
|
||
'<button type="button" class="btn-close" data-bs-dismiss="alert"></button>';
|
||
|
||
alertContainer.appendChild(alert);
|
||
|
||
// Автоскрытие через 5 секунд
|
||
setTimeout(() => {
|
||
if (alert.parentNode) {
|
||
alert.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
|
||
// ==========================================
|
||
// Вспомогательные функции для компонентов
|
||
// ==========================================
|
||
|
||
// Очистка результатов поиска
|
||
function clearSearchResults() {
|
||
const resultsContainer = document.getElementById('searchResults');
|
||
if (resultsContainer) {
|
||
resultsContainer.style.display = 'none';
|
||
}
|
||
|
||
const guideSelectorContainer = document.getElementById('guide-selector-container');
|
||
if (guideSelectorContainer) {
|
||
guideSelectorContainer.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Функция для быстрого бронирования (вызывается из компонентов)
|
||
function quickBookTour(routeId, guideId, date, peopleCount = 1) {
|
||
// Создаем модальное окно для быстрого бронирования
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal fade';
|
||
modal.innerHTML = `
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Бронирование тура</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="quickBookingForm" action="/bookings" method="POST">
|
||
<input type="hidden" name="route_id" value="${routeId}">
|
||
<input type="hidden" name="guide_id" value="${guideId}">
|
||
<input type="hidden" name="preferred_date" value="${date}">
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Ваше имя *</label>
|
||
<input type="text" class="form-control" name="customer_name" required>
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Количество человек</label>
|
||
<input type="number" class="form-control" name="people_count" value="${peopleCount}" min="1" max="20" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Email *</label>
|
||
<input type="email" class="form-control" name="customer_email" required>
|
||
</div>
|
||
<div class="col-md-6 mb-3">
|
||
<label class="form-label">Телефон *</label>
|
||
<input type="tel" class="form-control" name="customer_phone" required>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label">Особые пожелания</label>
|
||
<textarea class="form-control" name="special_requirements" rows="3"></textarea>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="submit" form="quickBookingForm" class="btn btn-primary">
|
||
<i class="fas fa-credit-card me-1"></i>Забронировать
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
const bootstrapModal = new bootstrap.Modal(modal);
|
||
bootstrapModal.show();
|
||
|
||
// Удаление модального окна после закрытия
|
||
modal.addEventListener('hidden.bs.modal', function() {
|
||
document.body.removeChild(modal);
|
||
});
|
||
}
|
||
|
||
// Делаем функции доступными глобально для использования в компонентах
|
||
window.clearSearchResults = clearSearchResults;
|
||
window.quickBookTour = quickBookTour;
|
||
|
||
// ==========================================
|
||
// Утилитарные функции (продолжение)
|
||
// ==========================================
|
||
|
||
// ==========================================
|
||
// Финальные утилитарные функции
|
||
// ==========================================
|
||
function createAlertContainer() {
|
||
const container = document.createElement('div');
|
||
container.id = 'alert-container';
|
||
container.className = 'position-fixed top-0 end-0 p-3';
|
||
container.style.zIndex = '9999';
|
||
document.body.appendChild(container);
|
||
return container;
|
||
}
|
||
|
||
// Функция для форматирования чисел (валюта)
|
||
function formatNumber(num) {
|
||
return new Intl.NumberFormat('ru-RU').format(num);
|
||
}
|
||
|
||
// Делаем утилитарные функции доступными глобально
|
||
window.formatNumber = formatNumber;
|
||
|
||
console.log('Korea Tourism Agency - JavaScript with components loaded successfully! 🇰🇷');
|
||
}); |