236 lines
11 KiB
JavaScript
236 lines
11 KiB
JavaScript
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: 'Вид на море' }
|
||
};
|
||
|
||
let cachedRooms = [];
|
||
|
||
async function loadRoomsPublic() {
|
||
try {
|
||
const res = await fetch('/api/rooms');
|
||
if (!res.ok) throw new Error('Failed to load rooms');
|
||
cachedRooms = await res.json();
|
||
renderRoomsPublic(cachedRooms);
|
||
updateRoomPrices(cachedRooms);
|
||
} catch (err) {
|
||
console.error('Error loading rooms:', err);
|
||
document.getElementById('roomsEmpty').style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function renderRoomsPublic(rooms) {
|
||
const grid = document.getElementById('roomsGrid');
|
||
const empty = document.getElementById('roomsEmpty');
|
||
|
||
if (!rooms || rooms.length === 0) {
|
||
grid.innerHTML = '';
|
||
empty.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
empty.style.display = 'none';
|
||
|
||
grid.innerHTML = rooms.map((room, index) => {
|
||
const amenities = Array.isArray(room.amenities) ? room.amenities : [];
|
||
const floors = Array.isArray(room.floors) ? room.floors : [];
|
||
const images = Array.isArray(room.images) ? room.images : [];
|
||
|
||
const primaryImg = images.find(i => i.is_primary) || images[0];
|
||
let imageSrc = room.image_path || 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 250"%3E%3Crect fill="%23274151" width="400" height="250"/%3E%3Ctext fill="%2364748b" font-family="sans-serif" font-size="16" x="50%25" y="50%25" text-anchor="middle" dy=".3em"%3EФото%3C/text%3E%3C/svg%3E';
|
||
if (primaryImg && primaryImg.image_path) {
|
||
imageSrc = primaryImg.image_path;
|
||
}
|
||
const fullImageSrc = imageSrc.startsWith('uploads') || imageSrc.startsWith('data') ? '/' + imageSrc : imageSrc;
|
||
|
||
const allImages = images.length > 0 ? images : (room.image_path ? [{ image_path: room.image_path, is_primary: 1 }] : []);
|
||
|
||
let imagesHtml = '';
|
||
if (allImages.length > 1) {
|
||
const dotsHtml = allImages.map((img, i) => {
|
||
const src = img.image_path.startsWith('uploads') || img.image_path.startsWith('data') ? '/' + img.image_path : img.image_path;
|
||
return `<span class="room-img-dot ${i === 0 ? 'active' : ''}" data-index="${i}" data-src="${escapeHtml(src)}"></span>`;
|
||
}).join('');
|
||
imagesHtml = `<div class="room-img-carousel">
|
||
<img src="${fullImageSrc}" alt="${room.name}" class="room-carousel-img" id="roomCarouselImg${index}"
|
||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 400 250%22%3E%3Crect fill=%22%23274151%22 width=%22400%22 height=%22250%22/%3E%3Ctext fill=%22%2364748b%22 font-family=%22sans-serif%22 font-size=%2216%22 x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22%3EФото%3C/text%3E%3C/svg%3E'">
|
||
<button class="room-carousel-prev" onclick="roomCarouselPrev(${index})">‹</button>
|
||
<button class="room-carousel-next" onclick="roomCarouselNext(${index})">›</button>
|
||
<div class="room-img-dots">${dotsHtml}</div>
|
||
</div>`;
|
||
} else {
|
||
imagesHtml = `<img src="${fullImageSrc}" alt="${room.name}" class="room-carousel-img"
|
||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 400 250%22%3E%3Crect fill=%22%23274151%22 width=%22400%22 height=%22250%22/%3E%3Ctext fill=%22%2364748b%22 font-family=%22sans-serif%22 font-size=%2216%22 x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22%3EФото%3C/text%3E%3C/svg%3E'">`;
|
||
}
|
||
|
||
const amenitiesHtml = amenities.map(a => {
|
||
const info = AMENITY_ICONS[a];
|
||
if (!info) return '';
|
||
return `<span class="room-feature-tag"><i class="fas ${info.icon}"></i> ${info.label}</span>`;
|
||
}).join('');
|
||
|
||
const floorsStr = floors.length > 0 ? 'Этажи ' + floors.join(', ') : '';
|
||
|
||
const extraBedsHtml = room.extra_beds > 0
|
||
? `<div class="room-extra-hint">Возможно +${room.extra_beds} доп. мест (${room.extra_bed_price} ₽)</div>`
|
||
: '';
|
||
|
||
const featuredClass = room.type === 'Семейный' ? ' featured' : '';
|
||
|
||
return `
|
||
<div class="col-lg-4">
|
||
<div class="room-card${featuredClass}">
|
||
<div class="room-image">
|
||
${imagesHtml}
|
||
<div class="room-category">${room.type}</div>
|
||
<div class="room-availability" id="roomAvail${index}" style="display:none;position:absolute;bottom:8px;left:8px;background:rgba(0,0,0,0.75);color:#fff;font-size:0.75rem;padding:3px 8px;border-radius:10px;"></div>
|
||
</div>
|
||
<div class="room-body">
|
||
<h3 class="room-name">${escapeHtml(room.name)}</h3>
|
||
<p class="room-desc">${escapeHtml(room.description || '')}</p>
|
||
<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>' : ''}
|
||
</div>
|
||
<div class="room-features">
|
||
${amenitiesHtml}
|
||
</div>
|
||
${extraBedsHtml}
|
||
<div class="room-footer">
|
||
<div class="room-price">
|
||
<span class="amount">от ${room.price_per_night || 0} ₽</span>
|
||
<span class="period"> / ночь</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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
initRoomBookingHandlers();
|
||
}
|
||
|
||
let roomCarouselState = {};
|
||
let roomCarouselTimers = {};
|
||
|
||
function roomCarouselPrev(index) {
|
||
if (!roomCarouselState[index]) roomCarouselState[index] = 0;
|
||
const imgs = getRoomImagesForIndex(index);
|
||
if (roomCarouselState[index] > 0) roomCarouselState[index]--;
|
||
else roomCarouselState[index] = imgs.length - 1;
|
||
applyCarouselFrame(index, imgs);
|
||
}
|
||
|
||
function roomCarouselNext(index) {
|
||
if (!roomCarouselState[index]) roomCarouselState[index] = 0;
|
||
const imgs = getRoomImagesForIndex(index);
|
||
if (roomCarouselState[index] < imgs.length - 1) roomCarouselState[index]++;
|
||
else roomCarouselState[index] = 0;
|
||
applyCarouselFrame(index, imgs);
|
||
}
|
||
|
||
function getRoomImagesForIndex(index) {
|
||
const room = cachedRooms[index];
|
||
if (!room) return [];
|
||
const images = Array.isArray(room.images) && room.images.length > 0 ? room.images : (room.image_path ? [{ image_path: room.image_path, is_primary: 1 }] : []);
|
||
return images;
|
||
}
|
||
|
||
function applyCarouselFrame(index, images) {
|
||
const img = document.getElementById('roomCarouselImg' + index);
|
||
const dots = document.querySelectorAll(`#roomCarouselImg${index} ~ .room-img-dots .room-img-dot`);
|
||
if (!img || images.length === 0) return;
|
||
const state = roomCarouselState[index];
|
||
const src = images[state].image_path.startsWith('uploads') || images[state].image_path.startsWith('data') ? '/' + images[state].image_path : images[state].image_path;
|
||
img.src = src;
|
||
dots.forEach((d, i) => d.classList.toggle('active', i === state));
|
||
}
|
||
|
||
function updateRoomPrices(rooms) {
|
||
const prices = {};
|
||
const maxGuests = {};
|
||
rooms.forEach(room => {
|
||
prices[room.type] = room.price_per_night;
|
||
maxGuests[room.type] = room.max_guests;
|
||
});
|
||
if (window.updateRoomPricesData) {
|
||
window.updateRoomPricesData(prices, maxGuests);
|
||
}
|
||
}
|
||
|
||
function initRoomBookingHandlers() {
|
||
document.querySelectorAll('.btn-book').forEach(btn => {
|
||
btn.addEventListener('click', function() {
|
||
const roomId = this.getAttribute('data-room-id');
|
||
const roomType = this.getAttribute('data-room-type');
|
||
const roomName = this.getAttribute('data-room-name');
|
||
const maxGuests = parseInt(this.getAttribute('data-max-guests')) || 4;
|
||
document.getElementById('selectedRoom').value = roomType;
|
||
document.getElementById('selectedRoomId').value = roomId;
|
||
updateGuestOptionsDynamic(roomType, maxGuests);
|
||
hidePriceInfo();
|
||
});
|
||
});
|
||
}
|
||
|
||
function updateGuestOptionsDynamic(roomType, maxGuests) {
|
||
const guestsSelect = document.querySelector('[name="guests"]');
|
||
if (!guestsSelect) return;
|
||
|
||
let options = [];
|
||
for (let i = 1; i <= maxGuests; i++) {
|
||
let text = i === 1 ? '1 гость' : (i < 5 ? `${i} гостя` : `${i} гостей`);
|
||
options.push(`<option value="${i}">${text}</option>`);
|
||
}
|
||
|
||
guestsSelect.innerHTML = options.join('');
|
||
}
|
||
|
||
async function checkRoomAvailability() {
|
||
const checkin = document.querySelector('[name="checkin"]').value;
|
||
const checkout = document.querySelector('[name="checkout"]').value;
|
||
if (!checkin || !checkout) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/rooms/availability?checkin=${checkin}&checkout=${checkout}`);
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
cachedRooms.forEach((room, i) => {
|
||
const avail = data.find(a => a.id === room.id);
|
||
const el = document.getElementById('roomAvail' + i);
|
||
if (el && avail) {
|
||
el.style.display = 'block';
|
||
if (avail.available === 0) {
|
||
el.textContent = 'Нет мест';
|
||
el.style.background = 'rgba(239,68,68,0.85)';
|
||
} else if (avail.available <= 2) {
|
||
el.textContent = 'Мест: ' + avail.available;
|
||
el.style.background = 'rgba(245,158,11,0.85)';
|
||
} else {
|
||
el.textContent = 'Свободно: ' + avail.available;
|
||
el.style.background = 'rgba(22,163,74,0.85)';
|
||
}
|
||
}
|
||
});
|
||
} catch (e) {}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadRoomsPublic();
|
||
});
|