Files
2026-10-20 09:18:59 +02:00

265 lines
12 KiB
JavaScript

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&current=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);