страна
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 43s

This commit is contained in:
2026-07-26 18:08:30 +05:00
parent f0da3d2b3f
commit 26f24d31af
6 changed files with 55 additions and 7 deletions

View File

@@ -26,7 +26,7 @@ const translations = {
'select.alphabetical': 'По алфавиту',
// Review Form
'form.select_country': 'Выберите страну',
'form.select_country': 'Выберите страну (необязательно)',
'form.select_city': 'Город (необязательно)',
'form.another_country': 'Другая страна',
'form.country_placeholder': 'Введите название страны',
@@ -355,7 +355,7 @@ const translations = {
'select.alphabetical': 'Alphabetical',
// Review Form
'form.select_country': 'Select country',
'form.select_country': 'Select country (optional)',
'form.select_city': 'City (optional)',
'form.another_country': 'Other country',
'form.country_placeholder': 'Enter country name',
@@ -684,7 +684,7 @@ const translations = {
'select.alphabetical': 'Алфавит ала',
// Review Form
'form.select_country': 'Атәыла алхразы',
'form.select_country': 'Атәыла алхразы (иахәҭахым)',
'form.select_city': 'Ақалақь (иахәҭахым)',
'form.another_country': 'Атәыла даҽаны',
'form.country_placeholder': 'Атәыла ахьӡ аҭазыргыла',

View File

@@ -1,5 +1,6 @@
const COUNTRIES = [
{ code: 'AF', name: 'Afghanistan', nameRu: 'Афганистан' },
{ code: 'AB', name: 'Abkhazia', nameRu: 'Абхазия' },
{ code: 'AL', name: 'Albania', nameRu: 'Албания' },
{ code: 'DZ', name: 'Algeria', nameRu: 'Алжир' },
{ code: 'AD', name: 'Andorra', nameRu: 'Андорра' },

View File

@@ -80,14 +80,14 @@
<div class="col-auto"><div class="stat-divider"></div></div>
<div class="col-auto">
<div class="stat-item">
<div class="stat-number"><span class="counter" data-target="3">0</span></div>
<div class="stat-number"><span class="counter" data-stat="categories" data-target="3">0</span></div>
<div class="stat-label" data-i18n="hero.stat_categories">категории</div>
</div>
</div>
<div class="col-auto"><div class="stat-divider"></div></div>
<div class="col-auto">
<div class="stat-item">
<div class="stat-number"><span class="counter" data-target="98">0</span>%</div>
<div class="stat-number"><span class="counter" data-stat="satisfaction" data-target="98">0</span>%</div>
<div class="stat-label" data-i18n="hero.stat_guests">довольных гостей</div>
</div>
</div>
@@ -936,7 +936,7 @@
<label data-i18n="form.select_country">Страна</label>
<div class="review-select-wrapper">
<select class="review-form-control" id="reviewCountry" required>
<option value="" data-i18n="form.select_country">Выберите страну</option>
<option value="" data-i18n="form.select_country">Выберите страну (необязательно)</option>
</select>
</div>
<div class="review-error" id="countryError" data-i18n="validation.country_required">Выберите страну</div>

View File

@@ -37,9 +37,22 @@ document.querySelectorAll('.animate-on-scroll').forEach(el => {
});
// Counter animation
const counterObserver = new IntersectionObserver((entries) => {
const statsPromise = fetch('/api/stats').then(r => r.ok ? r.json() : null).catch(() => null);
const counterObserver = new IntersectionObserver(async (entries) => {
const stats = await statsPromise;
entries.forEach(entry => {
if (entry.isIntersecting) {
if (stats) {
entry.target.querySelectorAll('.counter[data-stat]').forEach(el => {
const stat = el.getAttribute('data-stat');
if (stats[stat] !== undefined) {
el.setAttribute('data-target', stats[stat]);
}
});
}
const counters = entry.target.querySelectorAll('.counter');
counters.forEach(counter => {
const target = parseInt(counter.getAttribute('data-target'));

View File

@@ -140,13 +140,24 @@ 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 popularCodes = popularCountriesCache.map(c => c.country_code);
const popularCountries = [];
const otherCountries = [];
countriesCache.forEach(country => {
if (PINNED_CODES.includes(country.code)) return;
if (popularCodes.includes(country.code)) {
popularCountries.push(country);
} else {

View File

@@ -392,6 +392,28 @@ app.get('/api/translations/:lang', (req, res) => {
res.json(translations);
});
app.get('/api/stats', (req, res) => {
db.get(`SELECT COUNT(DISTINCT type) as count FROM rooms WHERE is_active = 1`, [], (err, row) => {
if (err) {
console.error('Stats categories error:', err);
return res.status(500).json({ error: 'Database error' });
}
const categories = (row && row.count > 0) ? row.count : Object.keys(config.ROOM_TYPES).length;
db.get(`SELECT COUNT(*) as total, SUM(CASE WHEN stars >= 4 THEN 1 ELSE 0 END) as satisfied FROM reviews WHERE is_approved = 1`, [], (err2, row2) => {
if (err2) {
console.error('Stats reviews error:', err2);
return res.status(500).json({ error: 'Database error' });
}
let satisfaction = 100;
if (row2 && row2.total > 0) {
satisfaction = Math.round((row2.satisfied / row2.total) * 100);
}
res.json({ categories, satisfaction });
});
});
});
app.get('/api/countries-cities', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'data', 'countries-cities.js'));
});
@@ -404,6 +426,7 @@ app.get('/api/countries', (req, res) => {
if (match) {
try {
let jsonStr = match[1];
jsonStr = jsonStr.replace(/(\{|\,)\s*(\w+):/g, '$1"$2":');
jsonStr = jsonStr.replace(/'/g, '"');
const countries = JSON.parse(jsonStr);
res.json(countries);