рус2
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 32s

This commit is contained in:
2026-07-18 17:42:50 +05:00
parent 3385781aa7
commit bdb30c9b20
4 changed files with 143 additions and 43 deletions

View File

@@ -290,7 +290,36 @@ const translations = {
'booking.base_price': 'Базовая стоимость:',
'booking.discount': 'Скидка',
'booking.total': 'Итого к оплате:',
'booking.submit': 'Отправить заявку'
'booking.submit': 'Отправить заявку',
'booking.sending': 'Отправка...',
'booking.success_sent': 'Заявка отправлена Рауфу Алексеевичу!',
'booking.send_error': 'Ошибка отправки',
'booking.server_error': 'Ошибка сервера',
'booking.checkin_past': 'Дата заезда не может быть в прошлом',
'booking.checkout_before_checkin': 'Дата выезда должна быть позже даты заезда',
'booking.promocode_invalid': 'Промокод не найден или срок его действия истёк',
'booking.promocode_error': 'Ошибка проверки промокода',
// Rooms JS
'rooms.floors': 'Этажи',
'rooms.extra_beds': 'Возможно',
'rooms.extra_beds_rest': 'доп. мест',
'rooms.from': 'от',
'rooms.per_night': '/ ночь',
'rooms.book': 'Забронировать',
'rooms.no_rooms': 'Нет мест',
'rooms.rooms_left': 'Мест:',
'rooms.available': 'Свободно:',
// Amenities
'amenity.ac': 'Кондиционер',
'amenity.tv': 'ТВ',
'amenity.fridge': 'Холодильник',
'amenity.wifi': 'WiFi',
'amenity.kettle': 'Чайник',
'amenity.hairdryer': 'Фен',
'amenity.shower': 'Душ в номере',
'amenity.sea_view': 'Вид на море'
},
en: {
@@ -584,7 +613,36 @@ const translations = {
'booking.base_price': 'Base price:',
'booking.discount': 'Discount',
'booking.total': 'Total to pay:',
'booking.submit': 'Send Request'
'booking.submit': 'Send Request',
'booking.sending': 'Sending...',
'booking.success_sent': 'Request sent to Rauf Alekseevich!',
'booking.send_error': 'Sending Error',
'booking.server_error': 'Server Error',
'booking.checkin_past': 'Check-in date cannot be in the past',
'booking.checkout_before_checkin': 'Check-out date must be after check-in date',
'booking.promocode_invalid': 'Promo code not found or expired',
'booking.promocode_error': 'Promo code check error',
// Rooms JS
'rooms.floors': 'Floors',
'rooms.extra_beds': 'Available',
'rooms.extra_beds_rest': 'extra beds',
'rooms.from': 'from',
'rooms.per_night': '/ night',
'rooms.book': 'Book',
'rooms.no_rooms': 'No rooms',
'rooms.rooms_left': 'Left:',
'rooms.available': 'Available:',
// Amenities
'amenity.ac': 'Air Conditioning',
'amenity.tv': 'TV',
'amenity.fridge': 'Fridge',
'amenity.wifi': 'WiFi',
'amenity.kettle': 'Kettle',
'amenity.hairdryer': 'Hair Dryer',
'amenity.shower': 'En-suite Shower',
'amenity.sea_view': 'Sea View'
},
ab: {
@@ -881,7 +939,36 @@ const translations = {
'booking.base_price': 'Абазатә ахә:',
'booking.discount': 'Аскидка',
'booking.total': 'Зегьы ашәахә:',
'booking.submit': 'Азаиака аашьҭра'
'booking.submit': 'Азаиака аашьҭра',
'booking.sending': 'Иаашьҭуеит...',
'booking.success_sent': 'Азаиака Рауф Алексееви иахь иаашьҭын!',
'booking.send_error': 'Аашьҭра агха',
'booking.server_error': 'Асервер агха',
'booking.checkin_past': 'Анеира арыцхә ааигәа иҟаӡам',
'booking.checkout_before_checkin': 'Алҵра арыцхә анеира арыцхә иашьҭахь акәзароуп',
'booking.promocode_invalid': 'Апромокод иԥшаам ма аамҭа иацәын',
'booking.promocode_error': 'Апромокод агәаҭара агха',
// Rooms JS
'rooms.floors': 'Аихақәа',
'rooms.extra_beds': 'Иаԥсоит',
'rooms.extra_beds_rest': 'доп. тыԥ',
'rooms.from': 'иқәынтәи',
'rooms.per_night': '/ ахә',
'rooms.book': 'Абронира',
'rooms.no_rooms': 'Апалаҭа ыҟам',
'rooms.rooms_left': 'Иаанхоит:',
'rooms.available': 'Иахьазы:',
// Amenities
'amenity.ac': 'Акондиционер',
'amenity.tv': 'АТВ',
'amenity.fridge': 'Аршәашьика',
'amenity.wifi': 'WiFi',
'amenity.kettle': 'Ачаиник',
'amenity.hairdryer': 'Афен',
'amenity.shower': 'Адуш апалаҭаҟны',
'amenity.sea_view': 'Амшын ахсаала'
}
};

View File

@@ -1,4 +1,7 @@
// 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');
@@ -83,7 +86,8 @@ function updateGuestOptionsDynamic(roomType, maxGuests) {
let options = [];
for (let i = 1; i <= maxGuests; i++) {
let text = i === 1 ? '1 гость' : (i < 5 ? `${i} гостя` : `${i} гостей`);
const key = 'booking.guest_' + i;
let text = rt(key, i === 1 ? '1 гость' : (i < 5 ? i + ' гостя' : i + ' гостей'));
options.push(`<option value="${i}">${text}</option>`);
}
@@ -102,7 +106,8 @@ function updateGuestOptions(room) {
const maxGuests = ROOM_MAX_GUESTS[room] || 4;
let options = [];
for (let i = 1; i <= maxGuests; i++) {
let text = i === 1 ? '1 гость' : (i < 5 ? `${i} гостя` : `${i} гостей`);
const key = 'booking.guest_' + i;
let text = rt(key, i === 1 ? '1 гость' : (i < 5 ? i + ' гостя' : i + ' гостей'));
options.push(`<option value="${i}">${text}</option>`);
}
@@ -114,21 +119,21 @@ function updateGuestOptionsFallback(room) {
if (!guestsSelect) return;
const options2x = [
{ value: 1, text: '1 гость' },
{ value: 2, text: '2 гостя' }
{ value: 1, text: rt('booking.guest_1', '1 гость') },
{ value: 2, text: rt('booking.guest_2', '2 гостя') }
];
const options3x = [
{ value: 1, text: '1 гость' },
{ value: 2, text: '2 гостя' },
{ value: 3, text: '3 гостя' }
{ 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: '1 гость' },
{ value: 2, text: '2 гостя' },
{ value: 3, text: '3 гостя' },
{ value: 4, text: '4 гостя' }
{ 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;
@@ -160,7 +165,7 @@ function validateBookingDates() {
if (checkinInput.value) {
const checkinDate = new Date(checkinInput.value);
if (checkinDate < today) {
checkinInput.setCustomValidity('Дата заезда не может быть в прошлом');
checkinInput.setCustomValidity(rt('booking.checkin_past', 'Дата заезда не может быть в прошлом'));
} else {
checkinInput.setCustomValidity('');
}
@@ -171,7 +176,7 @@ function validateBookingDates() {
const checkoutDate = new Date(checkoutInput.value);
if (checkoutDate <= checkinDate) {
checkoutInput.setCustomValidity('Дата выезда должна быть позже даты заезда');
checkoutInput.setCustomValidity(rt('booking.checkout_before_checkin', 'Дата выезда должна быть позже даты заезда'));
} else {
checkoutInput.setCustomValidity('');
}
@@ -237,11 +242,11 @@ async function checkPromocode() {
} else {
currentPromocodeData = null;
updatePriceDisplay(basePrice, 0, 0, basePrice);
showPromocodeError('Промокод не найден или срок его действия истёк');
showPromocodeError(rt('booking.promocode_invalid', 'Промокод не найден или срок его действия истёк'));
}
} catch (error) {
console.error('Promocode validation error:', error);
showPromocodeError('Ошибка проверки промокода');
showPromocodeError(rt('booking.promocode_error', 'Ошибка проверки промокода'));
}
}
@@ -277,7 +282,7 @@ document.getElementById('bookingForm').addEventListener('submit', async function
const btn = this.querySelector('.btn-submit-booking');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Отправка...';
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>' + rt('booking.sending', 'Отправка...');
btn.disabled = true;
const formData = {
@@ -301,7 +306,7 @@ document.getElementById('bookingForm').addEventListener('submit', async function
});
if (response.ok) {
btn.innerHTML = '<i class="fas fa-check me-2"></i>Заявка отправлена Рауфу Алексеевичу!';
btn.innerHTML = '<i class="fas fa-check me-2"></i>' + rt('booking.success_sent', 'Заявка отправлена Рауфу Алексеевичу!');
btn.style.background = '#25d366';
this.reset();
hidePriceInfo();
@@ -313,11 +318,11 @@ document.getElementById('bookingForm').addEventListener('submit', async function
}, 2500);
} else {
const errorData = await response.json();
throw new Error(errorData.error || 'Ошибка сервера');
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>Ошибка отправки';
btn.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>' + rt('booking.send_error', 'Ошибка отправки');
btn.style.background = '#c9302c';
setTimeout(() => {
btn.innerHTML = originalText;

View File

@@ -503,6 +503,9 @@ async function switchLang(lang) {
populateCountrySelect();
}
loadReviews();
if (typeof loadRoomsPublic === 'function') {
loadRoomsPublic();
}
document.documentElement.lang = lang;
}

View File

@@ -1,14 +1,18 @@
const AMENITY_ICONS = {
has_ac: { icon: 'fa-snowflake', label: 'Кондиционер' },
has_tv: { icon: 'fa-tv', label: 'ТВ' },
has_fridge: { icon: 'fa-sink', label: 'Холодильник' },
has_wifi: { icon: 'fa-wifi', label: 'WiFi' },
has_kettle: { icon: 'fa-mug-hot', label: 'Чайник' },
has_hairdryer: { icon: 'fa-wind', label: 'Фен' },
has_shower: { icon: 'fa-shower', label: 'Душ в номере' },
has_sea_view: { icon: 'fa-water', label: 'Вид на море' }
const AMENITY_KEYS = {
has_ac: { icon: 'fa-snowflake', key: 'amenity.ac' },
has_tv: { icon: 'fa-tv', key: 'amenity.tv' },
has_fridge: { icon: 'fa-sink', key: 'amenity.fridge' },
has_wifi: { icon: 'fa-wifi', key: 'amenity.wifi' },
has_kettle: { icon: 'fa-mug-hot', key: 'amenity.kettle' },
has_hairdryer: { icon: 'fa-wind', key: 'amenity.hairdryer' },
has_shower: { icon: 'fa-shower', key: 'amenity.shower' },
has_sea_view: { icon: 'fa-water', key: 'amenity.sea_view' }
};
function rt(key, fallback) {
return (window.I18n && typeof window.I18n.t === 'function') ? window.I18n.t(key) : fallback;
}
let cachedRooms = [];
async function loadRoomsPublic() {
@@ -69,15 +73,15 @@ function renderRoomsPublic(rooms) {
}
const amenitiesHtml = amenities.map(a => {
const info = AMENITY_ICONS[a];
const info = AMENITY_KEYS[a];
if (!info) return '';
return `<span class="room-feature-tag"><i class="fas ${info.icon}"></i> ${info.label}</span>`;
return `<span class="room-feature-tag"><i class="fas ${info.icon}"></i> ${rt(info.key, a)}</span>`;
}).join('');
const floorsStr = floors.length > 0 ? 'Этажи ' + floors.join(', ') : '';
const floorsStr = floors.length > 0 ? rt('rooms.floors', 'Этажи') + ' ' + floors.join(', ') : '';
const extraBedsHtml = room.extra_beds > 0
? `<div class="room-extra-hint">Возможно +${room.extra_beds} доп. мест (${room.extra_bed_price} ₽)</div>`
? `<div class="room-extra-hint">${rt('rooms.extra_beds', 'Возможно').replace('{n}', room.extra_beds).replace('{price}', room.extra_bed_price)} +${room.extra_beds} ${rt('rooms.extra_beds_rest', 'доп. мест')} (${room.extra_bed_price} ₽)</div>`
: '';
const featuredClass = room.type === 'Семейный' ? ' featured' : '';
@@ -96,7 +100,7 @@ function renderRoomsPublic(rooms) {
<div class="room-meta">
<span><i class="fas fa-door-open"></i> ${room.area_sqm || 20} м²</span>
${floorsStr ? `<span><i class="fas fa-layer-group"></i> ${floorsStr}</span>` : ''}
${amenities.includes('has_shower') ? '<span><i class="fas fa-shower"></i> Душ в номере</span>' : ''}
${amenities.includes('has_shower') ? '<span><i class="fas fa-shower"></i> ' + rt('amenity.shower', 'Душ в номере') + '</span>' : ''}
</div>
<div class="room-features">
${amenitiesHtml}
@@ -104,10 +108,10 @@ function renderRoomsPublic(rooms) {
${extraBedsHtml}
<div class="room-footer">
<div class="room-price">
<span class="amount">от ${room.price_per_night || 0} ₽</span>
<span class="period"> / ночь</span>
<span class="amount">${rt('rooms.from', 'от')} ${room.price_per_night || 0} ₽</span>
<span class="period"> ${rt('rooms.per_night', '/ ночь')}</span>
</div>
<button class="btn-book" data-bs-toggle="modal" data-bs-target="#bookingModal" data-room-id="${room.id}" data-room-type="${room.type}" data-room-name="${escapeHtml(room.name)}" data-max-guests="${room.max_guests}">Забронировать</button>
<button class="btn-book" data-bs-toggle="modal" data-bs-target="#bookingModal" data-room-id="${room.id}" data-room-type="${room.type}" data-room-name="${escapeHtml(room.name)}" data-max-guests="${room.max_guests}">${rt('rooms.book', 'Забронировать')}</button>
</div>
</div>
</div>
@@ -187,7 +191,8 @@ function updateGuestOptionsDynamic(roomType, maxGuests) {
let options = [];
for (let i = 1; i <= maxGuests; i++) {
let text = i === 1 ? '1 гость' : (i < 5 ? `${i} гостя` : `${i} гостей`);
const key = 'booking.guest_' + i;
let text = rt(key, i === 1 ? '1 гость' : (i < 5 ? i + ' гостя' : i + ' гостей'));
options.push(`<option value="${i}">${text}</option>`);
}
@@ -209,13 +214,13 @@ async function checkRoomAvailability() {
if (el && avail) {
el.style.display = 'block';
if (avail.available === 0) {
el.textContent = 'Нет мест';
el.textContent = rt('rooms.no_rooms', 'Нет мест');
el.style.background = 'rgba(239,68,68,0.85)';
} else if (avail.available <= 2) {
el.textContent = 'Мест: ' + avail.available;
el.textContent = rt('rooms.rooms_left', 'Мест:') + ' ' + avail.available;
el.style.background = 'rgba(245,158,11,0.85)';
} else {
el.textContent = 'Свободно: ' + avail.available;
el.textContent = rt('rooms.available', 'Свободно:') + ' ' + avail.available;
el.style.background = 'rgba(22,163,74,0.85)';
}
}