Files
hotell777_260507/public/js/main.js
kalugin66 d539e1f0c0
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 42s
апи
2026-07-21 20:06:43 +05:00

592 lines
20 KiB
JavaScript
Raw 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.
// Preloader
function rt(key, fallback) {
return (window.I18n && typeof window.I18n.t === 'function') ? window.I18n.t(key) : fallback;
}
window.addEventListener('load', () => {
setTimeout(() => {
document.getElementById('preloader').classList.add('hidden');
}, 1500);
});
// Navbar scroll effect
const navbar = document.querySelector('.navbar');
window.addEventListener('scroll', () => {
if (window.scrollY > 80) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
// Scroll animations
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animated');
}
});
}, observerOptions);
document.querySelectorAll('.animate-on-scroll').forEach(el => {
observer.observe(el);
});
// Counter animation
const counterObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const counters = entry.target.querySelectorAll('.counter');
counters.forEach(counter => {
const target = parseInt(counter.getAttribute('data-target'));
const duration = 2000;
const step = target / (duration / 16);
let current = 0;
const timer = setInterval(() => {
current += step;
if (current >= target) {
counter.textContent = target;
clearInterval(timer);
} else {
counter.textContent = Math.floor(current);
}
}, 16);
});
counterObserver.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
const statsSection = document.querySelector('.hero-stats');
if (statsSection) counterObserver.observe(statsSection);
// Set min date for checkin to today
const today = new Date().toISOString().split('T')[0];
const checkinInput = document.querySelector('[name="checkin"]');
const checkoutInput = document.querySelector('[name="checkout"]');
if (checkinInput) checkinInput.min = today;
if (checkoutInput) checkoutInput.min = today;
// Booking modal - set room name
document.querySelectorAll('.btn-book').forEach(btn => {
btn.addEventListener('click', function() {
const room = this.getAttribute('data-room');
document.getElementById('selectedRoom').value = room;
const maxGuests = parseInt(this.getAttribute('data-max-guests')) || ROOM_MAX_GUESTS[room] || 4;
updateGuestOptionsFallback(room);
hidePriceInfo();
});
});
function updateGuestOptionsDynamic(roomType, maxGuests) {
const guestsSelect = document.querySelector('[name="guests"]');
if (!guestsSelect) return;
let options = [];
for (let i = 1; i <= maxGuests; i++) {
const key = 'booking.guest_' + i;
let text = rt(key, i === 1 ? '1 гость' : (i < 5 ? i + ' гостя' : i + ' гостей'));
options.push(`<option value="${i}">${text}</option>`);
}
guestsSelect.innerHTML = options.join('');
}
window.updateRoomPricesData = function(prices, maxGuests) {
Object.assign(ROOM_PRICES, prices);
Object.assign(ROOM_MAX_GUESTS, maxGuests);
};
function updateGuestOptions(room) {
const guestsSelect = document.querySelector('[name="guests"]');
if (!guestsSelect) return;
const maxGuests = ROOM_MAX_GUESTS[room] || 4;
let options = [];
for (let i = 1; i <= maxGuests; i++) {
const key = 'booking.guest_' + i;
let text = rt(key, i === 1 ? '1 гость' : (i < 5 ? i + ' гостя' : i + ' гостей'));
options.push(`<option value="${i}">${text}</option>`);
}
guestsSelect.innerHTML = options.join('');
}
function updateGuestOptionsFallback(room) {
const guestsSelect = document.querySelector('[name="guests"]');
if (!guestsSelect) return;
const options2x = [
{ value: 1, text: rt('booking.guest_1', '1 гость') },
{ value: 2, text: rt('booking.guest_2', '2 гостя') }
];
const options3x = [
{ value: 1, text: rt('booking.guest_1', '1 гость') },
{ value: 2, text: rt('booking.guest_2', '2 гостя') },
{ value: 3, text: rt('booking.guest_3', '3 гостя') }
];
const optionsDefault = [
{ value: 1, text: rt('booking.guest_1', '1 гость') },
{ value: 2, text: rt('booking.guest_2', '2 гостя') },
{ value: 3, text: rt('booking.guest_3', '3 гостя') },
{ value: 4, text: rt('booking.guest_4', '4 гостя') }
];
let options = optionsDefault;
if (room === '2x-местный') options = options2x;
else if (room === '3х-местный') options = options3x;
guestsSelect.innerHTML = options.map(o => `<option value="${o.value}">${o.text}</option>`).join('');
}
function hideGuestOptionsUpdate() {}
// Price calculation
const ROOM_PRICES = { '2x-местный': 1500, '3х-местный': 2000, 'Семейный': 3000, 'Люкс': 4500 };
const ROOM_MAX_GUESTS = { '2x-местный': 2, '3х-местный': 3, 'Семейный': 4, 'Люкс': 4 };
let currentPromocodeData = null;
function calculateNights(checkin, checkout) {
const ci = new Date(checkin);
const co = new Date(checkout);
return Math.ceil((co - ci) / (1000 * 60 * 60 * 24));
}
function validateBookingDates() {
const checkinInput = document.querySelector('[name="checkin"]');
const checkoutInput = document.querySelector('[name="checkout"]');
const today = new Date();
today.setHours(0, 0, 0, 0);
if (checkinInput.value) {
const checkinDate = new Date(checkinInput.value);
if (checkinDate < today) {
checkinInput.setCustomValidity(rt('booking.checkin_past', 'Дата заезда не может быть в прошлом'));
} else {
checkinInput.setCustomValidity('');
}
}
if (checkinInput.value && checkoutInput.value) {
const checkinDate = new Date(checkinInput.value);
const checkoutDate = new Date(checkoutInput.value);
if (checkoutDate <= checkinDate) {
checkoutInput.setCustomValidity(rt('booking.checkout_before_checkin', 'Дата выезда должна быть позже даты заезда'));
} else {
checkoutInput.setCustomValidity('');
}
}
return checkinInput.checkValidity() && checkoutInput.checkValidity();
}
document.querySelector('[name="checkin"]').addEventListener('change', function() { validateBookingDates(); if (typeof checkRoomAvailability === 'function') checkRoomAvailability(); });
document.querySelector('[name="checkout"]').addEventListener('change', function() { validateBookingDates(); if (typeof checkRoomAvailability === 'function') checkRoomAvailability(); });
function updatePriceDisplay(basePrice, discountPercent, discountAmount, totalPrice) {
document.getElementById('basePriceDisplay').textContent = basePrice + ' ₽';
document.getElementById('discountPercentDisplay').textContent = discountPercent;
document.getElementById('discountAmountDisplay').textContent = '-' + discountAmount + ' ₽';
document.getElementById('totalPriceDisplay').textContent = totalPrice + ' ₽';
document.getElementById('priceInfo').style.display = 'block';
}
function hidePriceInfo() {
document.getElementById('priceInfo').style.display = 'none';
currentPromocodeData = null;
}
function getFormData() {
return {
room: document.getElementById('selectedRoom').value,
checkin: document.querySelector('[name="checkin"]').value,
checkout: document.querySelector('[name="checkout"]').value,
promocode: document.getElementById('promocodeInput').value.trim()
};
}
async function checkPromocode() {
const { room, checkin, checkout, promocode } = getFormData();
if (!room || !checkin || !checkout) {
hidePriceInfo();
return;
}
const guests = parseInt(document.querySelector('[name="guests"]').value) || 1;
const basePrice = ROOM_PRICES[room] ? ROOM_PRICES[room] * guests * calculateNights(checkin, checkout) : 0;
if (!promocode) {
updatePriceDisplay(basePrice, 0, 0, basePrice);
currentPromocodeData = null;
hidePromocodeError();
return;
}
try {
const response = await fetch('/api/promocodes/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: promocode, room_type: room, checkin, checkout, guests })
});
const data = await response.json();
if (data.valid) {
currentPromocodeData = data;
updatePriceDisplay(data.base_price, data.discount_percent, data.discount_amount, data.total_price);
hidePromocodeError();
} else {
currentPromocodeData = null;
updatePriceDisplay(basePrice, 0, 0, basePrice);
showPromocodeError(rt('booking.promocode_invalid', 'Промокод не найден или срок его действия истёк'));
}
} catch (error) {
console.error('Promocode validation error:', error);
showPromocodeError(rt('booking.promocode_error', 'Ошибка проверки промокода'));
}
}
function showPromocodeError(message) {
const errorDiv = document.getElementById('promocodeError');
if (errorDiv) {
errorDiv.textContent = '✖ ' + message;
errorDiv.style.display = 'block';
}
}
function hidePromocodeError() {
const errorDiv = document.getElementById('promocodeError');
if (errorDiv) {
errorDiv.style.display = 'none';
}
}
document.getElementById('checkPromocodeBtn').addEventListener('click', checkPromocode);
document.getElementById('promocodeInput').addEventListener('blur', checkPromocode);
document.querySelector('[name="checkin"]').addEventListener('change', checkPromocode);
document.querySelector('[name="checkout"]').addEventListener('change', checkPromocode);
document.querySelector('[name="guests"]').addEventListener('change', checkPromocode);
document.querySelectorAll('.btn-book').forEach(btn => {
btn.addEventListener('click', function() {
setTimeout(checkPromocode, 100);
});
});
// Form submission
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = this.querySelector('.btn-submit-booking');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>' + rt('booking.sending', 'Отправка...');
btn.disabled = true;
const formData = {
name: this.querySelector('[name="name"]').value,
phone: this.querySelector('[name="phone"]').value,
adults: parseInt(this.querySelector('[name="guests"]').value),
children: 0,
checkin: this.querySelector('[name="checkin"]').value,
checkout: this.querySelector('[name="checkout"]').value,
wishes: this.querySelector('[name="wishes"]').value,
room: document.getElementById('selectedRoom').value,
room_id: parseInt(document.getElementById('selectedRoomId').value) || null,
promocode: document.getElementById('promocodeInput').value.trim() || null
};
try {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (response.ok) {
btn.innerHTML = '<i class="fas fa-check me-2"></i>' + rt('booking.success_sent', 'Заявка отправлена Рауфу Алексеевичу!');
btn.style.background = '#25d366';
this.reset();
hidePriceInfo();
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '';
btn.disabled = false;
bootstrap.Modal.getInstance(document.getElementById('bookingModal')).hide();
}, 2500);
} else {
const errorData = await response.json();
throw new Error(errorData.error || rt('booking.server_error', 'Ошибка сервера'));
}
} catch (error) {
console.error('Booking error:', error);
btn.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>' + rt('booking.send_error', 'Ошибка отправки');
btn.style.background = '#c9302c';
setTimeout(() => {
btn.innerHTML = originalText;
btn.style.background = '';
btn.disabled = false;
}, 3000);
}
});
// Smooth scroll for nav links
document.querySelectorAll('a[href^="#"]:not([data-phone])').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Close mobile menu
const navCollapse = document.querySelector('.navbar-collapse');
if (navCollapse.classList.contains('show')) {
bootstrap.Collapse.getInstance(navCollapse).hide();
}
}
});
});
// Hero media slideshow
let heroSlides = [];
let heroCurrentIndex = 0;
let heroTimer = null;
let heroPaused = false;
async function initHeroSlideshow() {
const layer = document.getElementById('heroBgLayer');
if (!layer) return;
try {
const resp = await fetch('/api/hero');
if (resp.ok) {
const media = await resp.json();
if (media && media.length > 0) {
heroSlides = media;
}
}
} catch (e) {
console.log('Hero API not available, using default image');
}
if (heroSlides.length === 0) return;
layer.innerHTML = '';
const defaultImg = layer.querySelector('.hero-bg-default');
if (defaultImg) defaultImg.remove();
heroSlides.forEach((item, i) => {
if (item.type === 'image') {
const img = document.createElement('img');
img.className = 'hero-bg hero-bg-slide';
img.src = item.path;
img.alt = 'Hotel 777';
img.dataset.index = i;
if (i === 0) img.classList.add('active');
layer.appendChild(img);
} else if (item.type === 'video') {
const isYouTube = item.path.includes('youtube') || item.path.includes('youtu.be');
if (isYouTube) {
let embedUrl = item.path;
if (item.path.includes('watch?v=')) {
const videoId = item.path.split('watch?v=')[1]?.split('&')[0];
embedUrl = 'https://www.youtube.com/embed/' + videoId + '?autoplay=0&controls=0&mute=1&loop=0&playlist=' + videoId + '&enablejsapi=1';
}
if (item.path.includes('youtu.be/')) {
const videoId = item.path.split('youtu.be/')[1]?.split('?')[0];
embedUrl = 'https://www.youtube.com/embed/' + videoId + '?autoplay=0&controls=0&mute=1&loop=0&playlist=' + videoId + '&enablejsapi=1';
}
if (!embedUrl.includes('enablejsapi=1')) {
embedUrl += (embedUrl.includes('?') ? '&' : '?') + 'enablejsapi=1';
}
const iframe = document.createElement('iframe');
iframe.src = embedUrl;
iframe.allow = 'autoplay; encrypted-media';
iframe.allowFullscreen = false;
iframe.dataset.index = i;
iframe.dataset.type = 'youtube';
layer.appendChild(iframe);
} else {
const video = document.createElement('video');
video.src = item.path;
video.muted = true;
video.playsInline = true;
video.dataset.index = i;
video.dataset.type = 'video';
if (i === 0) video.classList.add('active');
layer.appendChild(video);
video.addEventListener('ended', () => onVideoEnded(i));
}
}
});
startHeroSlideshow();
}
function startHeroSlideshow() {
if (heroSlides.length <= 1) {
const firstSlide = document.querySelector('#heroBgLayer .active, #heroBgLayer img[data-index="0"], #heroBgLayer video[data-index="0"]');
if (firstSlide && firstSlide.tagName === 'VIDEO') {
firstSlide.play().catch(() => {});
}
return;
}
showHeroSlide(0);
}
function showHeroSlide(index) {
if (heroPaused) return;
const layer = document.getElementById('heroBgLayer');
if (!layer) return;
const allSlides = layer.querySelectorAll('img, video, iframe');
allSlides.forEach(s => s.classList.remove('active'));
const slide = layer.querySelector(`[data-index="${index}"]`);
if (!slide) return;
// Reset transform on all slides (for parallax cleanup)
allSlides.forEach(s => { if (s.tagName === 'IMG' || s.tagName === 'VIDEO') s.style.transform = ''; });
slide.classList.add('active');
heroCurrentIndex = index;
if (heroTimer) clearTimeout(heroTimer);
if (slide.tagName === 'VIDEO') {
slide.currentTime = 0;
slide.play().catch(() => {});
// 'ended' event will trigger next slide
} else if (slide.dataset.type === 'youtube') {
// Post message to YouTube iframe to play
const src = new URL(slide.src);
src.searchParams.set('autoplay', '1');
slide.src = src.toString();
// Wait ~30 seconds for YouTube video, then advance (no reliable end event without API)
heroTimer = setTimeout(() => goToNextSlide(), 30000);
} else {
// Image: show for 10 seconds
heroTimer = setTimeout(() => goToNextSlide(), 10000);
}
}
function onVideoEnded(index) {
if (heroCurrentIndex !== index) return;
goToNextSlide();
}
function goToNextSlide() {
if (heroPaused || heroSlides.length === 0) return;
const nextIndex = (heroCurrentIndex + 1) % heroSlides.length;
const prevSlide = document.querySelector(`#heroBgLayer [data-index="${heroCurrentIndex}"]`);
if (prevSlide && prevSlide.tagName === 'VIDEO') {
prevSlide.pause();
}
if (prevSlide && prevSlide.dataset.type === 'youtube') {
prevSlide.src = prevSlide.src.replace('autoplay=1', 'autoplay=0');
}
showHeroSlide(nextIndex);
}
document.addEventListener('visibilitychange', () => {
heroPaused = document.hidden;
if (!heroPaused && heroSlides.length > 1) {
showHeroSlide(heroCurrentIndex);
}
});
// Parallax effect on hero
window.addEventListener('scroll', () => {
const hero = document.querySelector('#heroBgLayer .active');
if (hero && (hero.tagName === 'IMG' || hero.tagName === 'VIDEO')) {
const scrolled = window.scrollY;
hero.style.transform = `scale(${1 + scrolled * 0.0001}) translateY(${scrolled * 0.3}px)`;
} else {
const fallback = document.querySelector('.hero-bg-default');
if (fallback) {
const scrolled = window.scrollY;
fallback.style.transform = `scale(${1 + scrolled * 0.0001}) translateY(${scrolled * 0.3}px)`;
}
}
});
document.addEventListener('DOMContentLoaded', () => {
initHeroSlideshow();
});
// Gallery Lightbox
function openLightbox(src, caption) {
const lb = document.getElementById('galleryLightbox');
const img = document.getElementById('galleryLightboxImg');
const cap = document.getElementById('galleryLightboxCaption');
img.src = src;
cap.textContent = caption || '';
lb.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
const lb = document.getElementById('galleryLightbox');
lb.classList.remove('active');
document.body.style.overflow = '';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeLightbox();
}
});
async function loadActivities() {
const container = document.getElementById('activitiesContainer');
if (!container) return;
const lang = (window.I18n && I18n.currentLang) ? I18n.currentLang : 'ru';
try {
const res = await fetch('/api/activities?lang=' + lang);
if (!res.ok) throw new Error('Failed to load activities');
const activities = await res.json();
if (!activities.length) {
container.innerHTML = '';
return;
}
container.innerHTML = activities.map(a => {
const imgSrc = a.image_path || '';
const iconHtml = a.icon ? `<div class="activity-icon"><i class="${a.icon}"></i></div>` : '';
const onclickAttr = imgSrc ? `onclick="openLightbox('${imgSrc}', '${(a.title || '').replace(/'/g, "\\'")}')"` : '';
return `<div class="col-lg-4 animate-on-scroll">
<div class="activity-card" ${onclickAttr}>
${imgSrc ? `<img src="${imgSrc}" alt="${a.title || ''}">` : ''}
<div class="activity-overlay">
${iconHtml}
<h3 class="activity-title">${a.title || ''}</h3>
<p class="activity-desc">${a.desc || ''}</p>
</div>
</div>
</div>`;
}).join('');
container.querySelectorAll('.animate-on-scroll').forEach(el => {
observer.observe(el);
});
} catch (err) {
console.error('Activities load error:', err);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadActivities);
} else {
loadActivities();
}