modified: index.html modified: package-lock.json modified: package.json modified: script.js modified: server.js deleted: style.css
544 lines
20 KiB
JavaScript
544 lines
20 KiB
JavaScript
// API ключ от WeatherAPI
|
||
const API_KEY = '485eff906f7d473b913104046250710';
|
||
|
||
const citySelect = document.getElementById('city');
|
||
const getWeatherBtn = document.getElementById('get-weather');
|
||
const timelapseContainer = document.getElementById('timelapse-container');
|
||
const currentWeatherDiv = document.getElementById('current-weather');
|
||
const currentTempEl = document.getElementById('current-temp');
|
||
const currentDescEl = document.getElementById('current-desc');
|
||
const currentHumidityEl = document.getElementById('current-humidity');
|
||
const currentWindEl = document.getElementById('current-wind');
|
||
|
||
// Глобальная переменная для Swiper
|
||
let weatherSwiper;
|
||
|
||
// Глобальные переменные для графиков
|
||
let temperatureChart = null;
|
||
let precipitationChart = null;
|
||
|
||
// Иконки погоды
|
||
const weatherIcons = {
|
||
'Sunny': 'sun',
|
||
'Clear': 'sun',
|
||
'Partly cloudy': 'cloud-sun',
|
||
'Cloudy': 'cloud',
|
||
'Overcast': 'cloud',
|
||
'Mist': 'cloud-fog',
|
||
'Fog': 'cloud-fog',
|
||
'Light rain': 'cloud-rain',
|
||
'Moderate rain': 'cloud-rain',
|
||
'Heavy rain': 'cloud-rain',
|
||
'Light snow': 'cloud-snow',
|
||
'Moderate snow': 'cloud-snow',
|
||
'Heavy snow': 'cloud-snow',
|
||
'Thunderstorm': 'cloud-lightning',
|
||
'Patchy rain possible': 'cloud-drizzle',
|
||
'Patchy snow possible': 'cloud-snow',
|
||
'Patchy sleet possible': 'cloud-snow',
|
||
'Patchy freezing drizzle possible': 'cloud-drizzle',
|
||
'Blowing snow': 'wind',
|
||
'Blizzard': 'wind',
|
||
'Freezing fog': 'cloud-fog',
|
||
'Patchy light drizzle': 'cloud-drizzle',
|
||
'Light drizzle': 'cloud-drizzle',
|
||
'Freezing drizzle': 'cloud-drizzle',
|
||
'Heavy freezing drizzle': 'cloud-drizzle',
|
||
'Patchy light rain': 'cloud-rain',
|
||
'Light rain shower': 'cloud-rain',
|
||
'Moderate or heavy rain shower': 'cloud-rain',
|
||
'Torrential rain shower': 'cloud-rain',
|
||
'Patchy light snow': 'cloud-snow',
|
||
'Light snow showers': 'cloud-snow',
|
||
'Moderate or heavy snow showers': 'cloud-snow',
|
||
'Patchy light rain with thunder': 'cloud-lightning',
|
||
'Moderate or heavy rain with thunder': 'cloud-lightning',
|
||
'Patchy light snow with thunder': 'cloud-lightning',
|
||
'Moderate or heavy snow with thunder': 'cloud-lightning'
|
||
};
|
||
|
||
// Функция для отображения текущей даты
|
||
function displayCurrentDate() {
|
||
const currentDateEl = document.getElementById('current-date');
|
||
const now = new Date();
|
||
|
||
const daysOfWeek = ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'];
|
||
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
|
||
|
||
const dayOfWeek = daysOfWeek[now.getDay()];
|
||
const day = now.getDate();
|
||
const month = months[now.getMonth()];
|
||
|
||
currentDateEl.textContent = `${dayOfWeek}, ${day} ${month}`;
|
||
}
|
||
|
||
// Функция для обновления заголовка страницы
|
||
function updatePageTitle() {
|
||
const selectedCity = citySelect.options[citySelect.selectedIndex].text;
|
||
document.title = `Погода в ${selectedCity}`;
|
||
}
|
||
|
||
// Автоматическая загрузка погоды при загрузке страницы
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Отображаем текущую дату
|
||
displayCurrentDate();
|
||
|
||
// Загружаем сохраненный город из localStorage
|
||
const savedCity = localStorage.getItem('selectedCity');
|
||
if (savedCity) {
|
||
citySelect.value = savedCity;
|
||
}
|
||
|
||
// Обновляем заголовок страницы
|
||
updatePageTitle();
|
||
|
||
// Ждем загрузки Swiper
|
||
if (typeof Swiper !== 'undefined') {
|
||
getWeatherBtn.click();
|
||
} else {
|
||
// Если Swiper еще не загрузился, ждем его
|
||
const checkSwiper = setInterval(() => {
|
||
if (typeof Swiper !== 'undefined') {
|
||
clearInterval(checkSwiper);
|
||
getWeatherBtn.click();
|
||
}
|
||
}, 100);
|
||
}
|
||
});
|
||
|
||
getWeatherBtn.addEventListener('click', async () => {
|
||
const city = citySelect.value;
|
||
|
||
// Сохраняем выбранный город в localStorage
|
||
localStorage.setItem('selectedCity', city);
|
||
|
||
const url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=7&hourly=true&lang=ru`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error('Ошибка сети');
|
||
}
|
||
const data = await response.json();
|
||
|
||
// Отображаем текущую погоду
|
||
displayCurrentWeather(data);
|
||
|
||
// Пересоздаем иконки после обновления DOM
|
||
setTimeout(() => {
|
||
lucide.createIcons();
|
||
}, 100);
|
||
|
||
// Таймлапс на сегодня
|
||
displayTimelapse(data.forecast.forecastday[0].hour, data.location.localtime);
|
||
|
||
// Погода на неделю
|
||
displayWeeklyWeather(data.forecast.forecastday);
|
||
|
||
// Графики температуры и осадков
|
||
createTemperatureChart(data.forecast.forecastday[0].hour);
|
||
createPrecipitationChart(data.forecast.forecastday[0].hour);
|
||
|
||
// Обновляем заголовок страницы с выбранным городом
|
||
updatePageTitle();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки данных:', error);
|
||
alert('Не удалось загрузить данные о погоде. Проверьте API ключ.');
|
||
}
|
||
});
|
||
|
||
function displayCurrentWeather(data) {
|
||
const current = data.current;
|
||
currentTempEl.textContent = `${Math.round(current.temp_c)}°C`;
|
||
currentDescEl.textContent = current.condition.text;
|
||
currentHumidityEl.textContent = `${current.humidity}%`;
|
||
currentWindEl.textContent = `${current.wind_kph} км/ч`;
|
||
|
||
// Показываем блок текущей погоды
|
||
currentWeatherDiv.classList.remove('hidden');
|
||
currentWeatherDiv.classList.add('animate-fade-in');
|
||
}
|
||
|
||
function displayTimelapse(hourlyData, localtime) {
|
||
timelapseContainer.innerHTML = '';
|
||
const currentHour = new Date(localtime).getHours();
|
||
let currentSlideIndex = -1;
|
||
|
||
hourlyData.forEach((item, index) => {
|
||
const slide = document.createElement('div');
|
||
const hour = new Date(item.time).getHours();
|
||
const isCurrent = hour === currentHour;
|
||
if (isCurrent) currentSlideIndex = index;
|
||
|
||
slide.className = `swiper-slide timelapse-item bg-gradient-to-br from-blue-50 to-indigo-50 p-3 rounded-xl shadow-md border ${isCurrent ? 'border-blue-500 bg-gradient-to-br from-blue-100 to-indigo-100 ring-2 ring-blue-300' : 'border-gray-100'}`;
|
||
slide.style.width = '140px';
|
||
const iconName = weatherIcons[item.condition.text] || 'cloud';
|
||
slide.innerHTML = `
|
||
<div class="text-center">
|
||
<p class="text-xs font-medium ${isCurrent ? 'text-blue-700 font-semibold' : 'text-gray-600'} mb-1">${hour}:00 ${isCurrent ? '(сейчас)' : ''}</p>
|
||
<div class="flex justify-center mb-2">
|
||
<i data-lucide="${iconName}" class="w-6 h-6 ${isCurrent ? 'text-blue-600' : 'text-blue-500'}"></i>
|
||
</div>
|
||
<p class="text-lg font-bold ${isCurrent ? 'text-blue-700' : 'text-blue-600'} mb-1">${Math.round(item.temp_c)}°C</p>
|
||
<p class="text-xs ${isCurrent ? 'text-blue-600' : 'text-gray-600'} mb-1">${item.condition.text}</p>
|
||
<p class="text-xs text-gray-500">💧 ${item.precip_mm} мм</p>
|
||
<p class="text-xs text-gray-500">☔ ${item.chance_of_rain}%</p>
|
||
</div>
|
||
`;
|
||
timelapseContainer.appendChild(slide);
|
||
});
|
||
|
||
// Пересоздаем иконки
|
||
setTimeout(() => {
|
||
lucide.createIcons();
|
||
}, 100);
|
||
|
||
// Инициализируем Swiper если он еще не создан
|
||
setTimeout(() => {
|
||
if (!weatherSwiper) {
|
||
weatherSwiper = new Swiper('.swiper', {
|
||
slidesPerView: window.innerWidth < 768 ? 3 : 6,
|
||
spaceBetween: 12,
|
||
grabCursor: true,
|
||
freeMode: true,
|
||
mousewheel: {
|
||
enabled: true,
|
||
sensitivity: 1,
|
||
},
|
||
slidesOffsetBefore: 0,
|
||
slidesOffsetAfter: 0,
|
||
centeredSlides: false,
|
||
watchSlidesProgress: true
|
||
});
|
||
} else {
|
||
weatherSwiper.update();
|
||
weatherSwiper.updateSlides();
|
||
}
|
||
|
||
// Фокус на текущем времени города
|
||
if (currentSlideIndex !== -1) {
|
||
setTimeout(() => {
|
||
weatherSwiper.slideTo(currentSlideIndex, 800);
|
||
}, 200);
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
function displayWeeklyWeather(forecastData) {
|
||
const weeklyContainer = document.getElementById('weekly-container');
|
||
weeklyContainer.innerHTML = '';
|
||
|
||
const daysOfWeek = ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'];
|
||
|
||
forecastData.forEach((day, index) => {
|
||
const date = new Date(day.date);
|
||
const dayOfWeek = daysOfWeek[date.getDay()];
|
||
const dayNumber = date.getDate();
|
||
const month = date.toLocaleDateString('ru-RU', { month: 'short' });
|
||
|
||
const iconName = weatherIcons[day.day.condition.text] || 'cloud';
|
||
const isToday = index === 0;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = `text-center p-4 rounded-xl border transition-all duration-200 hover:shadow-lg ${
|
||
isToday
|
||
? 'bg-blue-50 border-blue-300 ring-2 ring-blue-200'
|
||
: 'bg-gray-50 border-gray-200 hover:border-blue-300'
|
||
}`;
|
||
|
||
card.innerHTML = `
|
||
<div class="mb-2">
|
||
<p class="text-sm font-medium text-gray-600 ${isToday ? 'text-blue-700' : ''}">
|
||
${isToday ? 'Сегодня' : dayOfWeek}
|
||
</p>
|
||
<p class="text-xs text-gray-500">${dayNumber} ${month}</p>
|
||
</div>
|
||
<div class="flex justify-center mb-3">
|
||
<i data-lucide="${iconName}" class="w-8 h-8 ${isToday ? 'text-blue-600' : 'text-gray-600'}"></i>
|
||
</div>
|
||
<div class="mb-2">
|
||
<p class="text-lg font-bold ${isToday ? 'text-blue-700' : 'text-gray-800'}">
|
||
${Math.round(day.day.maxtemp_c)}°
|
||
</p>
|
||
<p class="text-sm text-gray-500">
|
||
${Math.round(day.day.mintemp_c)}°
|
||
</p>
|
||
</div>
|
||
<p class="text-xs ${isToday ? 'text-blue-600' : 'text-gray-600'}">
|
||
${day.day.condition.text}
|
||
</p>
|
||
`;
|
||
|
||
weeklyContainer.appendChild(card);
|
||
});
|
||
|
||
// Пересоздаем иконки для недельного прогноза
|
||
setTimeout(() => {
|
||
lucide.createIcons();
|
||
}, 100);
|
||
}
|
||
|
||
// Функция создания графика температуры
|
||
function createTemperatureChart(hourlyData) {
|
||
const ctx = document.getElementById('temperatureChart').getContext('2d');
|
||
|
||
// Подготавливаем данные
|
||
const labels = hourlyData.map(item => {
|
||
const hour = new Date(item.time).getHours();
|
||
return `${hour}:00`;
|
||
});
|
||
|
||
const temperatures = hourlyData.map(item => Math.round(item.temp_c));
|
||
|
||
// Уничтожаем предыдущий график если он существует
|
||
if (temperatureChart) {
|
||
temperatureChart.destroy();
|
||
}
|
||
|
||
// Создаем новый график
|
||
temperatureChart = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: 'Температура (°C)',
|
||
data: temperatures,
|
||
borderColor: '#3b82f6',
|
||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||
borderWidth: 3,
|
||
fill: true,
|
||
tension: 0.4,
|
||
pointBackgroundColor: '#3b82f6',
|
||
pointBorderColor: '#ffffff',
|
||
pointBorderWidth: 2,
|
||
pointRadius: 4,
|
||
pointHoverRadius: 6,
|
||
pointHoverBackgroundColor: '#2563eb',
|
||
pointHoverBorderColor: '#ffffff',
|
||
pointHoverBorderWidth: 2
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||
titleColor: '#ffffff',
|
||
bodyColor: '#ffffff',
|
||
cornerRadius: 8,
|
||
displayColors: false,
|
||
callbacks: {
|
||
title: function(context) {
|
||
return `Время: ${context[0].label}`;
|
||
},
|
||
label: function(context) {
|
||
return `Температура: ${context.parsed.y}°C`;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
grid: {
|
||
display: false
|
||
},
|
||
ticks: {
|
||
color: '#6b7280',
|
||
font: {
|
||
size: 12
|
||
}
|
||
}
|
||
},
|
||
y: {
|
||
grid: {
|
||
color: 'rgba(0, 0, 0, 0.1)',
|
||
lineWidth: 1
|
||
},
|
||
ticks: {
|
||
color: '#6b7280',
|
||
font: {
|
||
size: 12
|
||
},
|
||
callback: function(value) {
|
||
return value + '°C';
|
||
}
|
||
}
|
||
}
|
||
},
|
||
animation: {
|
||
duration: 2000,
|
||
easing: 'easeInOutQuart'
|
||
},
|
||
interaction: {
|
||
intersect: false,
|
||
mode: 'index'
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция создания графика осадков
|
||
function createPrecipitationChart(hourlyData) {
|
||
const ctx = document.getElementById('precipitationChart').getContext('2d');
|
||
|
||
// Подготавливаем данные
|
||
const labels = hourlyData.map(item => {
|
||
const hour = new Date(item.time).getHours();
|
||
return `${hour}:00`;
|
||
});
|
||
|
||
const precipitations = hourlyData.map(item => item.precip_mm);
|
||
const chances = hourlyData.map(item => item.chance_of_rain);
|
||
|
||
// Уничтожаем предыдущий график если он существует
|
||
if (precipitationChart) {
|
||
precipitationChart.destroy();
|
||
}
|
||
|
||
// Создаем новый график
|
||
precipitationChart = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: 'Осадки (мм)',
|
||
data: precipitations,
|
||
backgroundColor: 'rgba(59, 130, 246, 0.6)',
|
||
borderColor: '#3b82f6',
|
||
borderWidth: 1,
|
||
borderRadius: 4,
|
||
borderSkipped: false,
|
||
barThickness: 'flex',
|
||
maxBarThickness: 30
|
||
}, {
|
||
label: 'Вероятность (%)',
|
||
data: chances,
|
||
type: 'line',
|
||
borderColor: '#ef4444',
|
||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||
borderWidth: 2,
|
||
fill: false,
|
||
tension: 0.4,
|
||
pointBackgroundColor: '#ef4444',
|
||
pointBorderColor: '#ffffff',
|
||
pointBorderWidth: 2,
|
||
pointRadius: 3,
|
||
pointHoverRadius: 5,
|
||
pointHoverBackgroundColor: '#dc2626',
|
||
pointHoverBorderColor: '#ffffff',
|
||
pointHoverBorderWidth: 2,
|
||
yAxisID: 'y1'
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
display: false
|
||
},
|
||
tooltip: {
|
||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||
titleColor: '#ffffff',
|
||
bodyColor: '#ffffff',
|
||
cornerRadius: 8,
|
||
displayColors: true,
|
||
callbacks: {
|
||
title: function(context) {
|
||
return `Время: ${context[0].label}`;
|
||
},
|
||
label: function(context) {
|
||
if (context.datasetIndex === 0) {
|
||
return `Осадки: ${context.parsed.y} мм`;
|
||
} else {
|
||
return `Вероятность: ${context.parsed.y}%`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
grid: {
|
||
display: false
|
||
},
|
||
ticks: {
|
||
color: '#6b7280',
|
||
font: {
|
||
size: 12
|
||
}
|
||
}
|
||
},
|
||
y: {
|
||
type: 'linear',
|
||
display: true,
|
||
position: 'left',
|
||
grid: {
|
||
color: 'rgba(0, 0, 0, 0.1)',
|
||
lineWidth: 1
|
||
},
|
||
ticks: {
|
||
color: '#6b7280',
|
||
font: {
|
||
size: 12
|
||
},
|
||
callback: function(value) {
|
||
return value + ' мм';
|
||
}
|
||
},
|
||
title: {
|
||
display: true,
|
||
text: 'Осадки (мм)',
|
||
color: '#3b82f6',
|
||
font: {
|
||
size: 14,
|
||
weight: 'bold'
|
||
}
|
||
}
|
||
},
|
||
y1: {
|
||
type: 'linear',
|
||
display: true,
|
||
position: 'right',
|
||
grid: {
|
||
drawOnChartArea: false
|
||
},
|
||
ticks: {
|
||
color: '#ef4444',
|
||
font: {
|
||
size: 12
|
||
},
|
||
callback: function(value) {
|
||
return value + '%';
|
||
}
|
||
},
|
||
title: {
|
||
display: true,
|
||
text: 'Вероятность (%)',
|
||
color: '#ef4444',
|
||
font: {
|
||
size: 14,
|
||
weight: 'bold'
|
||
}
|
||
}
|
||
}
|
||
},
|
||
animation: {
|
||
duration: 2000,
|
||
easing: 'easeInOutQuart',
|
||
delay: function(context) {
|
||
return context.dataIndex * 100;
|
||
}
|
||
},
|
||
interaction: {
|
||
intersect: false,
|
||
mode: 'index'
|
||
}
|
||
}
|
||
});
|
||
}
|