const translations = window.translations; const languageLocales = { vi: 'vi_VN', ru: 'ru_RU', en: 'en_US', zh: 'zh_CN', de: 'de_DE', fr: 'fr_FR' }; let currentLang = 'vi'; let currentWeatherCondition = 'weather_loading'; let tomorrowWeatherCondition = 'weather_loading'; function getWeatherPresentation(code) { if (code === 0) return { key: 'weather_clear', icon: 'fa-sun', color: 'text-amber-500' }; if (code === 1) return { key: 'weather_mainly_clear', icon: 'fa-cloud-sun', color: 'text-amber-500' }; if (code === 2) return { key: 'weather_partly_cloudy', icon: 'fa-cloud-sun', color: 'text-amber-500' }; if (code === 3) return { key: 'weather_overcast', icon: 'fa-cloud', color: 'text-gray-500' }; if ([45, 48].includes(code)) return { key: 'weather_fog', icon: 'fa-smog', color: 'text-gray-500' }; if ([51, 53, 55, 56, 57].includes(code)) return { key: 'weather_drizzle', icon: 'fa-cloud-rain', color: 'text-sky-500' }; if ([61, 63, 65, 66, 67].includes(code)) return { key: 'weather_rain', icon: 'fa-cloud-rain', color: 'text-sky-600' }; if ([71, 73, 75, 77].includes(code)) return { key: 'weather_snow', icon: 'fa-snowflake', color: 'text-sky-500' }; if ([80, 81, 82, 85, 86].includes(code)) return { key: 'weather_showers', icon: 'fa-cloud-showers-heavy', color: 'text-sky-600' }; if ([95, 96, 99].includes(code)) return { key: 'weather_thunderstorm', icon: 'fa-cloud-bolt', color: 'text-indigo-500' }; return { key: 'weather_partly_cloudy', icon: 'fa-cloud-sun', color: 'text-amber-500' }; } function hasLanguage(lang) { return Object.hasOwn(translations, lang); } function translateWeather(key) { return translations[currentLang][key] || translations.en[key]; } function renderWeatherCondition() { const condition = document.getElementById('weather-condition'); if (condition) condition.textContent = translateWeather(currentWeatherCondition); const currentForecastCondition = document.getElementById('weather-current-condition'); if (currentForecastCondition) currentForecastCondition.textContent = translateWeather(currentWeatherCondition); const tomorrowForecastCondition = document.getElementById('weather-tomorrow-condition'); if (tomorrowForecastCondition) tomorrowForecastCondition.textContent = translateWeather(tomorrowWeatherCondition); } function formatForecastDate(isoDate, lang) { const [year, month, day] = isoDate.split('-').map(Number); const date = new Date(year, month - 1, day, 12); return new Intl.DateTimeFormat(languageLocales[lang].replace('_', '-'), { weekday: 'short', month: 'short', day: 'numeric' }).format(date); } function isValidForecast(current, daily) { return Number.isFinite(current?.temperature_2m) && Number.isFinite(current?.weather_code) && Array.isArray(daily?.time) && daily.time.length >= 2 && /^\d{4}-\d{2}-\d{2}$/.test(daily.time[1]) && [daily.weather_code, daily.temperature_2m_max, daily.temperature_2m_min].every(values => Array.isArray(values) && values.length >= 2) && Number.isFinite(daily.weather_code[1]) && [daily.temperature_2m_max[0], daily.temperature_2m_min[0], daily.temperature_2m_max[1], daily.temperature_2m_min[1]].every(Number.isFinite); } function setWeatherRange(id, value) { const element = document.getElementById(id); if (element) element.textContent = Number.isFinite(value) ? `${Math.round(value)}°` : '--°'; } function setWeatherIcon(id, presentation) { const icon = document.getElementById(id); if (icon) icon.className = `fa-solid ${presentation.icon} text-4xl ${presentation.color}`; } async function loadWeatherForecast() { const temperature = document.getElementById('weather-temperature'); const icon = document.getElementById('weather-icon'); const currentForecastTemperature = document.getElementById('weather-current-temperature'); const tomorrowDate = document.getElementById('weather-tomorrow-date'); try { const response = await fetch('https://api.open-meteo.com/v1/forecast?latitude=12.2388&longitude=109.1967¤t=temperature_2m,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=Asia%2FHo_Chi_Minh&forecast_days=2'); if (!response.ok) throw new Error('Weather request failed'); const { current, daily } = await response.json(); if (!isValidForecast(current, daily)) throw new Error('Weather response is incomplete'); const currentPresentation = getWeatherPresentation(current.weather_code); const tomorrowPresentation = getWeatherPresentation(daily.weather_code[1]); const currentTemperature = `${Math.round(current.temperature_2m)}°C`; if (temperature) temperature.textContent = currentTemperature; if (currentForecastTemperature) currentForecastTemperature.textContent = currentTemperature; if (icon) icon.className = `fa-solid ${currentPresentation.icon} text-2xl ${currentPresentation.color}`; setWeatherIcon('weather-current-icon', currentPresentation); setWeatherIcon('weather-tomorrow-icon', tomorrowPresentation); setWeatherRange('weather-today-high', daily.temperature_2m_max[0]); setWeatherRange('weather-today-low', daily.temperature_2m_min[0]); setWeatherRange('weather-tomorrow-high', daily.temperature_2m_max[1]); setWeatherRange('weather-tomorrow-low', daily.temperature_2m_min[1]); if (tomorrowDate) { tomorrowDate.dateTime = daily.time[1]; tomorrowDate.textContent = formatForecastDate(daily.time[1], currentLang); } currentWeatherCondition = currentPresentation.key; tomorrowWeatherCondition = tomorrowPresentation.key; } catch { if (temperature) temperature.textContent = '--°C'; if (currentForecastTemperature) currentForecastTemperature.textContent = '--°C'; if (icon) icon.className = 'fa-solid fa-cloud text-2xl text-gray-400'; setWeatherIcon('weather-current-icon', { icon: 'fa-cloud', color: 'text-gray-400' }); setWeatherIcon('weather-tomorrow-icon', { icon: 'fa-cloud', color: 'text-gray-400' }); setWeatherRange('weather-today-high', NaN); setWeatherRange('weather-today-low', NaN); setWeatherRange('weather-tomorrow-high', NaN); setWeatherRange('weather-tomorrow-low', NaN); if (tomorrowDate) { tomorrowDate.textContent = ''; tomorrowDate.removeAttribute('datetime'); } currentWeatherCondition = 'weather_unavailable'; tomorrowWeatherCondition = 'weather_unavailable'; } renderWeatherCondition(); } function getPreferredLanguage() { const supported = Object.keys(translations); const preferences = navigator.languages?.length ? navigator.languages : [navigator.language]; for (const locale of preferences) { const language = locale.toLowerCase().split(/[-_]/)[0]; if (supported.includes(language)) return language; } return 'vi'; } function getRequestedLanguage() { const requested = new URLSearchParams(window.location.search).get('lang'); return hasLanguage(requested) ? requested : null; } function setLanguage(lang) { if (!hasLanguage(lang)) lang = 'vi'; currentLang = lang; document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if (translations[lang] && translations[lang][key]) { el.textContent = translations[lang][key]; } }); // Reset language button styles Object.keys(translations).forEach(l => { const btn = document.getElementById(`btn-${l}`); if (btn) { if (l === lang) { btn.className = "px-2.5 py-1.5 font-bold rounded-lg transition bg-white text-brand-600 shadow-sm"; } else { btn.className = "px-2.5 py-1.5 font-bold rounded-lg transition text-gray-600 hover:text-brand-600"; } } }); document.documentElement.lang = lang; const languageUrl = new URL(window.location.href); languageUrl.searchParams.set('lang', lang); history.replaceState(null, '', `${languageUrl.pathname}${languageUrl.search}${languageUrl.hash}`); const canonicalUrl = new URL(`/?lang=${lang}`, window.location.origin).href; document.getElementById('canonical-url').href = canonicalUrl; document.getElementById('meta-description').content = translations[lang].meta_description; document.getElementById('og-title').content = translations[lang].page_title; document.getElementById('og-description').content = translations[lang].meta_description; document.getElementById('og-url').content = canonicalUrl; document.getElementById('og-locale').content = languageLocales[lang]; renderWeatherCondition(); const tomorrowDate = document.getElementById('weather-tomorrow-date'); if (tomorrowDate.dateTime) tomorrowDate.textContent = formatForecastDate(tomorrowDate.dateTime, lang); } function toggleMobileMenu() { const menu = document.getElementById('mobile-menu'); menu.classList.toggle('hidden'); document.getElementById('mobile-menu-toggle').setAttribute('aria-expanded', !menu.classList.contains('hidden')); } function filterAttractions(category, activeButton) { document.querySelectorAll('.filter-btn').forEach(btn => { btn.className = "filter-btn px-5 py-2 rounded-xl text-sm font-bold bg-white text-gray-600 hover:bg-gray-100 shadow-sm"; }); activeButton.className = "filter-btn active px-5 py-2 rounded-xl text-sm font-bold bg-brand-500 text-white shadow"; document.querySelectorAll('.attraction-card').forEach(card => { if (category === 'all' || card.getAttribute('data-category') === category) { card.style.display = 'flex'; } else { card.style.display = 'none'; } }); } async function handleBooking(event) { event.preventDefault(); const form = event.currentTarget; const submitButton = form.querySelector('[type="submit"]'); const status = document.getElementById('form-status'); status.classList.add('hidden'); submitButton.disabled = true; try { const response = await fetch(form.action, { method: form.method, body: new FormData(form), headers: { Accept: 'application/json' } }); if (!response.ok) throw new Error('Form submission failed'); form.reset(); document.getElementById('success-modal').classList.remove('hidden'); } catch { status.classList.remove('hidden'); } finally { submitButton.disabled = false; } } function closeModal() { document.getElementById('success-modal').classList.add('hidden'); } function bindEvents() { document.querySelectorAll('[data-lang]').forEach(btn => { btn.addEventListener('click', () => setLanguage(btn.dataset.lang)); }); document.getElementById('mobile-menu-toggle').addEventListener('click', toggleMobileMenu); document.querySelectorAll('.mobile-nav-link').forEach(link => { link.addEventListener('click', toggleMobileMenu); }); document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', () => filterAttractions(btn.dataset.filter, btn)); }); document.getElementById('booking-form').addEventListener('submit', handleBooking); document.getElementById('modal-close-btn').addEventListener('click', closeModal); } bindEvents(); setLanguage(getRequestedLanguage() || getPreferredLanguage()); loadWeatherForecast(); setInterval(loadWeatherForecast, 30 * 60 * 1000);