514 lines
17 KiB
JavaScript
514 lines
17 KiB
JavaScript
async function loadReviews() {
|
|
const lang = I18n.currentLang;
|
|
try {
|
|
const res = await fetch(`/api/reviews?lang=${lang}`);
|
|
if (!res.ok) throw new Error('Failed to load reviews');
|
|
|
|
const data = await res.json();
|
|
renderReviews(data.reviews);
|
|
} catch (err) {
|
|
console.error('Error loading reviews:', err);
|
|
document.getElementById('reviewsEmpty').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
async function loadCountries() {
|
|
try {
|
|
const res = await fetch('/api/countries');
|
|
const data = await res.json();
|
|
if (Array.isArray(data)) {
|
|
return data;
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading countries:', err);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
async function loadCities(countryCode) {
|
|
try {
|
|
const res = await fetch(`/api/cities/${countryCode}`);
|
|
const data = await res.json();
|
|
|
|
const result = {
|
|
popular: data.popular || [],
|
|
cities: data.cities || []
|
|
};
|
|
|
|
if (result.popular.length > 0 || result.cities.length > 0) {
|
|
return result;
|
|
}
|
|
} catch (err) {
|
|
console.error('Error loading cities:', err);
|
|
}
|
|
return { popular: [], cities: [] };
|
|
}
|
|
|
|
function renderReviews(reviews) {
|
|
const grid = document.getElementById('reviewsGrid');
|
|
const emptyEl = document.getElementById('reviewsEmpty');
|
|
|
|
if (!grid || !emptyEl) return;
|
|
|
|
if (!reviews || reviews.length === 0) {
|
|
grid.innerHTML = '';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
emptyEl.style.display = 'none';
|
|
|
|
grid.innerHTML = reviews.map(review => {
|
|
const initials = I18n.getInitials(review.author_name);
|
|
const stars = I18n.renderStarsStatic(review.stars);
|
|
const date = review.created_at ? I18n.formatDate(review.created_at) : '';
|
|
const countryPart = review.country || '';
|
|
const cityPart = review.city ? `, ${review.city}` : '';
|
|
const location = countryPart || cityPart ? `${countryPart}${cityPart}` : '';
|
|
|
|
return `
|
|
<div class="review-card">
|
|
<div class="review-card-header">
|
|
<div class="review-author">
|
|
<div class="review-avatar">${initials}</div>
|
|
<div class="review-author-info">
|
|
<div class="review-author-name">${escapeHtml(review.author_name)}</div>
|
|
${location ? `<div class="review-author-location">${escapeHtml(location)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="review-stars">${stars}</div>
|
|
</div>
|
|
<p class="review-text">«${escapeHtml(review.text)}»</p>
|
|
${date ? `<div class="review-date">${date}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function openReviewModal() {
|
|
const modal = document.getElementById('reviewModal');
|
|
if (!modal) return;
|
|
|
|
await loadCountriesAndCities();
|
|
modal.classList.add('show');
|
|
document.body.style.overflow = 'hidden';
|
|
initStarsSlider();
|
|
resetReviewForm();
|
|
}
|
|
|
|
function closeReviewModal() {
|
|
const modal = document.getElementById('reviewModal');
|
|
if (!modal) return;
|
|
|
|
modal.classList.remove('show');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
let countriesCache = [];
|
|
let popularCountriesCache = [];
|
|
|
|
async function loadPopularCountries() {
|
|
try {
|
|
const res = await fetch('/api/reviews/popular');
|
|
const data = await res.json();
|
|
return data;
|
|
} catch (err) {
|
|
console.error('Error loading popular countries:', err);
|
|
}
|
|
return { countries: [], cities: {} };
|
|
}
|
|
|
|
async function loadCountriesAndCities() {
|
|
const popularData = await loadPopularCountries();
|
|
popularCountriesCache = popularData.countries || [];
|
|
|
|
if (countriesCache.length === 0) {
|
|
countriesCache = await loadCountries();
|
|
}
|
|
populateCountrySelect();
|
|
}
|
|
|
|
function populateCountrySelect() {
|
|
const select = document.getElementById('reviewCountry');
|
|
if (!select) return;
|
|
|
|
const PINNED_CODES = ['RU', 'AB'];
|
|
|
|
select.innerHTML = '<option value="">' + I18n.t('form.select_country') + '</option>';
|
|
|
|
const pinnedCountries = countriesCache.filter(c => PINNED_CODES.includes(c.code));
|
|
pinnedCountries.forEach(country => {
|
|
const opt = document.createElement('option');
|
|
opt.value = country.code;
|
|
opt.textContent = I18n.currentLang === 'ru' ? country.nameRu : country.name;
|
|
select.appendChild(opt);
|
|
});
|
|
|
|
const otherCountryOpt = document.createElement('option');
|
|
otherCountryOpt.value = '__other__';
|
|
otherCountryOpt.textContent = I18n.t('form.another_country');
|
|
select.appendChild(otherCountryOpt);
|
|
}
|
|
|
|
let currentCountryCode = null;
|
|
let citiesCache = [];
|
|
|
|
function setupCountryChangeHandler() {
|
|
const countrySelect = document.getElementById('reviewCountry');
|
|
if (countrySelect) {
|
|
countrySelect.addEventListener('change', async function() {
|
|
const countryCode = this.value;
|
|
const citySelect = document.getElementById('reviewCity');
|
|
const cityWrapper = document.getElementById('citySelectWrapper');
|
|
const otherCityGroup = document.getElementById('otherCityGroup');
|
|
const otherCountryGroup = document.getElementById('otherCountryGroup');
|
|
|
|
if (countryCode === '__other__') {
|
|
otherCountryGroup.style.display = 'block';
|
|
document.getElementById('reviewOtherCountry').focus();
|
|
citySelect.innerHTML = '<option value="">' + I18n.t('form.select_city') + '</option>';
|
|
citySelect.disabled = true;
|
|
cityWrapper.style.display = 'block';
|
|
otherCityGroup.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
otherCountryGroup.style.display = 'none';
|
|
|
|
if (!countryCode) {
|
|
citySelect.innerHTML = '<option value="">' + I18n.t('form.select_city') + '</option>';
|
|
citySelect.disabled = true;
|
|
otherCityGroup.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
citySelect.innerHTML = '<option value="">' + I18n.t('form.select_city') + '</option>';
|
|
citySelect.disabled = true;
|
|
|
|
const cityData = await loadCities(countryCode);
|
|
|
|
if (cityData.popular.length > 0) {
|
|
const groupOpt = document.createElement('optgroup');
|
|
groupOpt.label = I18n.t('select.popular');
|
|
cityData.popular.forEach(city => {
|
|
const opt = document.createElement('option');
|
|
opt.value = city;
|
|
opt.textContent = city;
|
|
groupOpt.appendChild(opt);
|
|
});
|
|
citySelect.appendChild(groupOpt);
|
|
}
|
|
|
|
if (cityData.cities.length > 0) {
|
|
const alphaOpt = document.createElement('optgroup');
|
|
alphaOpt.label = I18n.t('select.alphabetical');
|
|
cityData.cities.forEach(city => {
|
|
const opt = document.createElement('option');
|
|
opt.value = city;
|
|
opt.textContent = city;
|
|
alphaOpt.appendChild(opt);
|
|
});
|
|
citySelect.appendChild(alphaOpt);
|
|
}
|
|
|
|
const otherOpt = document.createElement('option');
|
|
otherOpt.value = '__other__';
|
|
otherOpt.textContent = I18n.t('form.another_city');
|
|
citySelect.appendChild(otherOpt);
|
|
|
|
citySelect.disabled = false;
|
|
cityWrapper.style.display = 'block';
|
|
otherCityGroup.style.display = 'none';
|
|
currentCountryCode = countryCode;
|
|
});
|
|
}
|
|
}
|
|
|
|
function setupCityChangeHandler() {
|
|
const citySelect = document.getElementById('reviewCity');
|
|
if (citySelect) {
|
|
citySelect.addEventListener('change', function() {
|
|
const otherCityGroup = document.getElementById('otherCityGroup');
|
|
if (this.value === '__other__') {
|
|
otherCityGroup.style.display = 'block';
|
|
document.getElementById('reviewOtherCity').focus();
|
|
} else {
|
|
otherCityGroup.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function initStarsSlider() {
|
|
const slider = document.getElementById('starsSlider');
|
|
const display = document.getElementById('starsDisplay');
|
|
const valueEl = document.getElementById('starsValue');
|
|
|
|
if (!slider || !display || !valueEl) return;
|
|
|
|
function updateStars(val) {
|
|
const stars = parseFloat(val);
|
|
const fullStars = Math.floor(stars);
|
|
const hasHalf = (stars % 1) >= 0.5;
|
|
const percent = (stars / 5) * 100;
|
|
|
|
slider.style.setProperty('--value', `${percent}%`);
|
|
valueEl.textContent = stars.toFixed(1);
|
|
|
|
let html = '';
|
|
for (let i = 0; i < 5; i++) {
|
|
if (i < fullStars) {
|
|
html += '<i class="fas fa-star filled"></i>';
|
|
} else if (i === fullStars && hasHalf) {
|
|
html += '<i class="fas fa-star-half-alt filled"></i>';
|
|
} else {
|
|
html += '<i class="far fa-star"></i>';
|
|
}
|
|
}
|
|
display.innerHTML = html;
|
|
}
|
|
|
|
slider.addEventListener('input', () => updateStars(slider.value));
|
|
updateStars(slider.value);
|
|
}
|
|
|
|
function resetReviewForm() {
|
|
const form = document.getElementById('reviewForm');
|
|
const countryEl = document.getElementById('reviewCountry');
|
|
const cityEl = document.getElementById('reviewCity');
|
|
const cityWrapper = document.getElementById('citySelectWrapper');
|
|
const otherCityGroup = document.getElementById('otherCityGroup');
|
|
const otherCountryGroup = document.getElementById('otherCountryGroup');
|
|
const starsSlider = document.getElementById('starsSlider');
|
|
const modalBody = document.getElementById('reviewModalBody');
|
|
|
|
if (form) form.reset();
|
|
if (countryEl) countryEl.value = '';
|
|
if (cityEl) cityEl.innerHTML = '<option value="">' + I18n.t('form.select_city') + '</option>';
|
|
if (cityWrapper) cityWrapper.style.display = 'block';
|
|
if (otherCityGroup) otherCityGroup.style.display = 'none';
|
|
if (otherCountryGroup) otherCountryGroup.style.display = 'none';
|
|
if (starsSlider) starsSlider.value = 5;
|
|
|
|
if (form) {
|
|
form.querySelectorAll('.review-error').forEach(el => el.classList.remove('show'));
|
|
form.querySelectorAll('.review-form-control').forEach(el => el.classList.remove('error'));
|
|
}
|
|
|
|
const successEl = document.getElementById('reviewSuccessMessage');
|
|
if (successEl) successEl.remove();
|
|
|
|
if (modalBody && form) {
|
|
modalBody.innerHTML = form.outerHTML;
|
|
I18n.updateUI();
|
|
}
|
|
|
|
initStarsSlider();
|
|
setupReviewFormSubmit();
|
|
setupCountryChangeHandler();
|
|
setupCityChangeHandler();
|
|
}
|
|
|
|
function getReviewCity() {
|
|
const citySelect = document.getElementById('reviewCity');
|
|
if (!citySelect || citySelect.value === '' || citySelect.value === '__other__') {
|
|
return document.getElementById('reviewOtherCity').value.trim() || '';
|
|
}
|
|
return citySelect.value;
|
|
}
|
|
|
|
function getReviewCountry() {
|
|
const select = document.getElementById('reviewCountry');
|
|
if (!select) return '';
|
|
if (select.value === '__other__') {
|
|
return document.getElementById('reviewOtherCountry').value.trim() || '';
|
|
}
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
return selectedOption ? selectedOption.textContent : '';
|
|
}
|
|
|
|
function validateReviewForm() {
|
|
const form = document.getElementById('reviewForm');
|
|
let isValid = true;
|
|
|
|
if (!form) return false;
|
|
|
|
form.querySelectorAll('.review-error').forEach(el => el.classList.remove('show'));
|
|
form.querySelectorAll('.review-form-control').forEach(el => el.classList.remove('error'));
|
|
|
|
const countryVal = document.getElementById('reviewCountry').value;
|
|
if (countryVal === '__other__') {
|
|
const otherCountry = document.getElementById('reviewOtherCountry').value.trim();
|
|
if (!otherCountry) {
|
|
document.getElementById('reviewOtherCountry').classList.add('error');
|
|
document.getElementById('countryError').classList.add('show');
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
const city = getReviewCity();
|
|
|
|
const nameEl = document.getElementById('reviewName');
|
|
const name = nameEl ? nameEl.value.trim() : '';
|
|
if (!name || name.length < 2) {
|
|
if (nameEl) nameEl.classList.add('error');
|
|
const nameError = nameEl ? nameEl.nextElementSibling : null;
|
|
if (nameError) nameError.classList.add('show');
|
|
isValid = false;
|
|
}
|
|
|
|
const textEl = document.getElementById('reviewText');
|
|
const text = textEl ? textEl.value.trim() : '';
|
|
if (!text || text.length < 20) {
|
|
if (textEl) textEl.classList.add('error');
|
|
document.getElementById('textError').classList.add('show');
|
|
isValid = false;
|
|
}
|
|
|
|
const codeEl = document.getElementById('reviewCode');
|
|
const code = codeEl ? codeEl.value.trim() : '';
|
|
if (!code) {
|
|
if (codeEl) codeEl.classList.add('error');
|
|
const codeError = document.getElementById('codeError');
|
|
if (codeError) {
|
|
codeError.textContent = I18n.t('validation.code_required');
|
|
codeError.classList.add('show');
|
|
}
|
|
isValid = false;
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
function setupReviewFormSubmit() {
|
|
const form = document.getElementById('reviewForm');
|
|
if (!form) return;
|
|
|
|
form.addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
if (!validateReviewForm()) return;
|
|
|
|
const submitBtn = document.getElementById('reviewSubmitBtn');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="spinner"></span><span>' + I18n.t('btn.loading') + '</span>';
|
|
}
|
|
|
|
const nameEl = document.getElementById('reviewName');
|
|
const countryEl = document.getElementById('reviewCountry');
|
|
const starsEl = document.getElementById('starsSlider');
|
|
const textEl = document.getElementById('reviewText');
|
|
const codeEl = document.getElementById('reviewCode');
|
|
|
|
const data = {
|
|
author_name: nameEl ? nameEl.value.trim() : '',
|
|
country_code: countryEl ? countryEl.value : '',
|
|
country_name: getReviewCountry(),
|
|
city: getReviewCity(),
|
|
stars: starsEl ? parseFloat(starsEl.value) : 5,
|
|
text: textEl ? textEl.value.trim() : '',
|
|
review_code: codeEl ? codeEl.value.trim() : ''
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/reviews', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
const result = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(result.error || 'Failed to submit review');
|
|
}
|
|
|
|
showReviewSuccess();
|
|
|
|
} catch (err) {
|
|
const codeError = document.getElementById('codeError');
|
|
if (err.message.includes('code') || err.message.includes('Invalid')) {
|
|
if (codeError) codeError.textContent = I18n.t('validation.code_invalid');
|
|
} else if (err.message.includes('recently')) {
|
|
if (codeError) codeError.textContent = I18n.t('validation.too_frequent');
|
|
} else {
|
|
if (codeError) codeError.textContent = err.message;
|
|
}
|
|
if (codeError) codeError.classList.add('show');
|
|
const codeInput = document.getElementById('reviewCode');
|
|
if (codeInput) codeInput.classList.add('error');
|
|
} finally {
|
|
const submitBtn = document.getElementById('reviewSubmitBtn');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = '<span>' + I18n.t('form.submit') + '</span>';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function showReviewSuccess() {
|
|
const body = document.getElementById('reviewModalBody');
|
|
if (!body) return;
|
|
|
|
const successText = I18n.t('validation.success');
|
|
const exIdx = successText.indexOf('!');
|
|
const title = exIdx > 0 ? successText.substring(0, exIdx + 1) : successText;
|
|
const desc = exIdx > 0 ? successText.substring(exIdx + 1).trim() : '';
|
|
body.innerHTML = `
|
|
<div class="review-success-message">
|
|
<i class="fas fa-check-circle"></i>
|
|
<h4>${title}</h4>
|
|
<p>${desc}</p>
|
|
</div>
|
|
`;
|
|
|
|
setTimeout(() => {
|
|
closeReviewModal();
|
|
}, 3000);
|
|
}
|
|
|
|
async function switchLang(lang) {
|
|
await I18n.setLang(lang);
|
|
document.title = I18n.t('page.title');
|
|
if (countriesCache.length > 0) {
|
|
populateCountrySelect();
|
|
}
|
|
loadReviews();
|
|
if (typeof loadRoomsPublic === 'function') {
|
|
loadRoomsPublic();
|
|
}
|
|
document.documentElement.lang = lang;
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await I18n.init();
|
|
setupReviewFormSubmit();
|
|
setupCountryChangeHandler();
|
|
setupCityChangeHandler();
|
|
loadReviews();
|
|
});
|
|
|
|
const reviewModal = document.getElementById('reviewModal');
|
|
if (reviewModal) {
|
|
reviewModal.addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeReviewModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
closeReviewModal();
|
|
}
|
|
}); |