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 `
${initials}
${escapeHtml(review.author_name)}
${location ? `
${escapeHtml(location)}
` : ''}
${stars}

«${escapeHtml(review.text)}»

${date ? `
${date}
` : ''}
`; }).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 = ''; 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 = ''; citySelect.disabled = true; otherCityGroup.style.display = 'none'; return; } citySelect.innerHTML = ''; 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 += ''; } else if (i === fullStars && hasHalf) { html += ''; } else { html += ''; } } 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 = ''; 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 = 'Отправка...'; } 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 = 'Отправить отзыв'; } } }); } function showReviewSuccess() { const body = document.getElementById('reviewModalBody'); if (!body) return; body.innerHTML = `

${I18n.t('validation.success').split('.')[0]}

${I18n.t('validation.success').split('.')[1] || ''}

`; 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(); } });