Files
tourrism_site/public/js/main.js
Andrey K. Choi 13c752b93a feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование
- Удалены дублирующие настройки navigation для чистой группировки
- Добавлены CSS стили для визуального отображения иерархии с отступами
- Добавлены эмодзи-иконки для каждого типа ресурсов через CSS
- Улучшена навигация с правильной вложенностью элементов
2025-11-30 21:57:58 +09:00

588 lines
24 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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! 🇰🇷');
});