Files
hotell777_260507/public/js/reviews.js
2026-05-10 21:42:31 +05:00

550 lines
19 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, data.stats);
} 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, stats) {
const grid = document.getElementById('reviewsGrid');
const statsEl = document.getElementById('reviewsStats');
const emptyEl = document.getElementById('reviewsEmpty');
if (!grid || !emptyEl) return;
if (!reviews || reviews.length === 0) {
if (statsEl) statsEl.style.display = 'none';
grid.innerHTML = '';
emptyEl.style.display = 'block';
return;
}
emptyEl.style.display = 'none';
if (statsEl) statsEl.style.display = 'flex';
const avgStars = stats.avgStars.toFixed(1);
const avgNumEl = document.getElementById('reviewsAvgNumber');
const avgStarsEl = document.getElementById('reviewsAvgStars');
const avgCountEl = document.getElementById('reviewsAvgCount');
if (avgNumEl) avgNumEl.textContent = avgStars;
if (avgStarsEl) avgStarsEl.innerHTML = I18n.renderStarsStatic(parseFloat(avgStars));
if (avgCountEl) avgCountEl.textContent = `${stats.count} ${getReviewWord(stats.count)}`;
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 animate-on-scroll">
<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('');
setTimeout(() => {
document.querySelectorAll('.review-card.animate-on-scroll').forEach(el => {
el.classList.add('visible');
});
}, 100);
}
function getReviewWord(count) {
const lastTwo = count % 100;
const lastOne = count % 10;
if (I18n.currentLang === 'en') {
return lastTwo === 11 ? 'reviews' : lastOne === 1 ? 'review' : 'reviews';
}
if (lastTwo >= 11 && lastTwo <= 14) return 'отзывов';
if (lastOne === 1) return 'отзыв';
if (lastOne >= 2 && lastOne <= 4) return 'отзыва';
return 'отзывов';
}
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;
select.innerHTML = '<option value="">' + I18n.t('form.select_country') + '</option>';
const popularCodes = popularCountriesCache.map(c => c.country_code);
const popularCountries = [];
const otherCountries = [];
countriesCache.forEach(country => {
if (popularCodes.includes(country.code)) {
popularCountries.push(country);
} else {
otherCountries.push(country);
}
});
otherCountries.sort((a, b) => {
const nameA = I18n.currentLang === 'ru' ? a.nameRu : a.name;
const nameB = I18n.currentLang === 'ru' ? b.nameRu : b.name;
return nameA.localeCompare(nameB);
});
if (popularCountries.length > 0) {
const popularGroup = document.createElement('optgroup');
popularGroup.label = I18n.t('select.popular');
popularCountries.forEach(country => {
const opt = document.createElement('option');
opt.value = country.code;
opt.textContent = I18n.currentLang === 'ru' ? country.nameRu : country.name;
popularGroup.appendChild(opt);
});
select.appendChild(popularGroup);
}
const otherGroup = document.createElement('optgroup');
otherGroup.label = I18n.t('select.alphabetical');
otherCountries.forEach(country => {
const opt = document.createElement('option');
opt.value = country.code;
opt.textContent = I18n.currentLang === 'ru' ? country.nameRu : country.name;
otherGroup.appendChild(opt);
});
select.appendChild(otherGroup);
}
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');
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 toggleCodeVisibility() {
const input = document.getElementById('reviewCode');
const btn = document.querySelector('.toggle-code i');
if (input.type === 'password') {
input.type = 'text';
btn.className = 'fas fa-eye-slash';
} else {
input.type = 'password';
btn.className = 'fas fa-eye';
}
}
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 starsSlider = document.getElementById('starsSlider');
const codeInput = document.getElementById('reviewCode');
const toggleBtn = document.querySelector('.toggle-code i');
const modalBody = document.getElementById('reviewModalBody');
if (form) form.reset();
if (countryEl) countryEl.value = '';
if (cityEl) cityEl.innerHTML = '<option value="">Выберите город</option>';
if (cityWrapper) cityWrapper.style.display = 'block';
if (otherCityGroup) otherCityGroup.style.display = 'none';
if (starsSlider) starsSlider.value = 5;
if (codeInput) codeInput.type = 'password';
if (toggleBtn) toggleBtn.className = 'fas fa-eye';
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;
}
initStarsSlider();
setupReviewFormSubmit();
}
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 '';
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 country = document.getElementById('reviewCountry').value;
if (!country) {
document.getElementById('reviewCountry').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>Отправка...</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 data-i18n="form.submit">Отправить отзыв</span>';
}
}
});
}
function showReviewSuccess() {
const body = document.getElementById('reviewModalBody');
if (!body) return;
body.innerHTML = `
<div class="review-success-message">
<i class="fas fa-check-circle"></i>
<h4>${I18n.t('validation.success').split('.')[0]}</h4>
<p>${I18n.t('validation.success').split('.')[1] || ''}</p>
</div>
`;
setTimeout(() => {
closeReviewModal();
}, 3000);
}
async function switchLang(lang) {
await I18n.setLang(lang);
loadReviews();
}
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();
}
});