550 lines
19 KiB
JavaScript
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();
|
|
}
|
|
}); |