new file: README.md

modified:   index.html
	modified:   package-lock.json
	modified:   package.json
	modified:   script.js
	modified:   server.js
	deleted:    style.css
This commit is contained in:
Fovway 2025-10-07 22:02:35 +07:00
parent dce99ec5dd
commit 096dac61aa
7 changed files with 767 additions and 340 deletions

57
README.md Normal file
View File

@ -0,0 +1,57 @@
# Weather App
Приложение для просмотра погоды.
## Описание
Это веб-приложение, которое отображает текущую погоду и почасовой прогноз с использованием API WeatherAPI.com. Интерфейс построен на Tailwind CSS для современного дизайна.
## Функциональность
- Отображение текущей погоды (температура, влажность, ветер)
- Таймлапс погоды на день
- Адаптивный дизайн
- Выбор городов
## Технологии
- Node.js
- Express
- Tailwind CSS
- HTML5
- JavaScript
- Swiper.js (для слайдера)
- Chart.js (для графиков)
- Lucide Icons (для иконок)
- Google Fonts (Inter)
## Установка
1. Клонируйте репозиторий:
```bash
git clone <url>
cd weather-app
```
2. Установите зависимости:
```bash
npm install
```
3. Запустите сервер:
```bash
npm start
```
4. Откройте http://localhost:3000 в браузере.
## Структура проекта
- `server.js` - сервер на Express
- `index.html` - главная страница
- `script.js` - клиентский JavaScript
- `package.json` - конфигурация
## Лицензия
MIT

View File

@ -4,29 +4,198 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Погода</title> <title>Погода</title>
<link rel="stylesheet" href="style.css"> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><defs><style>.a{fill:%233b82f6;}.b{fill:%23ffffff;}</style></defs><circle class='a' cx='16' cy='16' r='12'/><circle class='b' cx='16' cy='16' r='8'/><path class='a' d='M16,4V6M16,26V28M28,16H26M6,16H4M22.5,9.5l-1.5,1.5M8.5,22.5l-1.5,1.5M22.5,22.5l-1.5-1.5M8.5,9.5l-1.5-1.5' stroke='%233b82f6' stroke-width='1.5' fill='none'/><ellipse class='a' cx='20' cy='24' rx='8' ry='4' opacity='0.7'/></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet"> <script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<!-- Swiper CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: 'Inter', sans-serif; }
.swiper-slide { height: auto; }
.timelapse-container { padding-top: 12px; padding-bottom: 12px; }
.timelapse-item { transition: all 0.3s ease; }
.timelapse-item:hover { transform: translateY(-4px); box-shadow: 0 10px 25px rgba(0,0,0,0.1); }
/* Стили для графиков */
.chart-container {
position: relative;
height: 300px;
width: 100%;
}
.chart-container canvas {
max-height: 300px;
width: 100% !important;
height: 100% !important;
}
@media (max-width: 1024px) {
.chart-container {
height: 250px;
}
}
@media (max-width: 768px) {
.chart-container {
height: 200px;
}
}
</style>
</head> </head>
<body> <body class="bg-gradient-to-br from-blue-50 via-white to-indigo-50 min-h-screen">
<div class="container"> <div class="container mx-auto max-w-7xl px-4 py-8">
<header> <!-- Header -->
<h1>Погода</h1> <header class="text-center mb-12">
<div class="flex items-center justify-center gap-3 mb-4">
<div class="p-3 bg-blue-500 rounded-full">
<i data-lucide="cloud-sun" class="w-8 h-8 text-white"></i>
</div>
<h1 class="text-4xl md:text-5xl font-bold text-gray-800">Погода</h1>
</div>
<p class="text-gray-600 text-lg">Узнайте погоду в вашем городе</p>
<div class="mt-4 flex items-center justify-center gap-2 text-gray-700">
<i data-lucide="calendar" class="w-5 h-5"></i>
<span class="font-medium" id="current-date">Загрузка...</span>
</div>
</header> </header>
<div class="city-selector">
<label for="city">Выберите город:</label> <!-- City Selector -->
<select id="city"> <div class="bg-white rounded-2xl shadow-xl p-8 mb-12 border border-gray-100">
<option value="Novosibirsk">Новосибирск</option> <div class="flex flex-col md:flex-row items-center gap-6">
<div class="flex items-center gap-3 text-gray-700">
<i data-lucide="map-pin" class="w-5 h-5"></i>
<label for="city" class="font-medium text-lg">Выберите город:</label>
</div>
<select id="city" class="flex-1 px-6 py-3 border-2 border-gray-200 rounded-xl focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200 bg-white text-gray-700 font-medium min-w-[200px]">
<option value="Moscow">Москва</option> <option value="Moscow">Москва</option>
<option value="Saint Petersburg">Санкт-Петербург</option>
<option value="Novosibirsk">Новосибирск</option>
<option value="Yekaterinburg">Екатеринбург</option>
<option value="Nizhny Novgorod">Нижний Новгород</option>
<option value="Kazan">Казань</option>
<option value="Chelyabinsk">Челябинск</option>
<option value="Omsk">Омск</option>
<option value="Samara">Самара</option>
<option value="Rostov-on-Don">Ростов-на-Дону</option>
</select> </select>
<button id="get-weather">Получить погоду</button> <button id="get-weather" class="px-8 py-3 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-xl transition-all duration-200 transform hover:scale-105 shadow-lg hover:shadow-xl flex items-center gap-2">
<i data-lucide="search" class="w-5 h-5"></i>
Выбрать
</button>
</div> </div>
</div>
<!-- Weather Info -->
<div class="weather-info"> <div class="weather-info">
<div class="timelapse-weather"> <!-- Current Weather Section -->
<h2>Таймлапс погоды</h2> <div id="current-weather" class="hidden bg-white rounded-2xl shadow-xl p-8 mb-8 border border-gray-100">
<div id="timelapse-container" class="timelapse-container"></div> <div class="text-center">
<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center justify-center gap-2">
<i data-lucide="thermometer" class="w-6 h-6"></i>
Текущая погода
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="thermometer" class="w-5 h-5 text-blue-500"></i>
<p class="text-4xl font-bold text-blue-500" id="current-temp">--°C</p>
</div>
<p class="text-gray-600" id="current-desc">Загрузка...</p>
</div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="droplets" class="w-5 h-5 text-blue-500"></i>
<div>
<p class="text-lg text-gray-600">Влажность</p>
<p class="text-2xl font-semibold text-gray-800" id="current-humidity">--%</p>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center">
<div class="flex items-center justify-center gap-2 mb-2">
<i data-lucide="wind" class="w-5 h-5 text-blue-500"></i>
<div>
<p class="text-lg text-gray-600">Ветер</p>
<p class="text-2xl font-semibold text-gray-800" id="current-wind">-- км/ч</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Timelapse Section -->
<div class="timelapse-weather bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2">
<i data-lucide="clock" class="w-6 h-6"></i>
Погода на весь день
</h2>
<div class="swiper timelapse-container">
<div id="timelapse-container" class="swiper-wrapper"></div>
</div>
</div>
<!-- Weekly Weather Section -->
<div class="weekly-weather bg-white rounded-2xl shadow-xl p-8 border border-gray-100 mt-8">
<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2">
<i data-lucide="calendar-days" class="w-6 h-6"></i>
Погода на неделю
</h2>
<div class="grid grid-cols-1 md:grid-cols-7 gap-4" id="weekly-container">
<!-- Карточки недельного прогноза будут добавлены сюда -->
</div>
</div>
<!-- Charts Section -->
<div class="charts-section bg-white rounded-2xl shadow-xl p-8 border border-gray-100 mt-8">
<h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2">
<i data-lucide="bar-chart-3" class="w-6 h-6"></i>
Графики погоды
</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Temperature Chart -->
<div class="chart-container">
<h3 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<i data-lucide="thermometer" class="w-5 h-5 text-blue-500"></i>
Температура (°C)
</h3>
<canvas id="temperatureChart"></canvas>
</div>
<!-- Precipitation Chart -->
<div class="chart-container">
<h3 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<i data-lucide="cloud-rain" class="w-5 h-5 text-blue-500"></i>
Осадки (мм)
</h3>
<canvas id="precipitationChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="mt-8 mb-8 text-center">
<div class="text-gray-500 text-sm">
<p class="mb-1">Разработано: <span class="font-medium">Fovway</span></p>
<p>
<i data-lucide="mail" class="w-3 h-3 inline mr-1"></i>
<a href="mailto:admin@fovway.ru" class="hover:text-blue-500 transition-colors duration-200">
admin@fovway.ru
</a>
</p>
</div>
</footer>
<script src="script.js"></script> <script src="script.js"></script>
<!-- Swiper JS -->
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
lucide.createIcons();
</script>
</body> </body>
</html> </html>

9
package-lock.json generated
View File

@ -10,6 +10,9 @@
"dependencies": { "dependencies": {
"express": "^4.17.1" "express": "^4.17.1"
}, },
"devDependencies": {
"tailwindcss": "^4.1.14"
},
"engines": { "engines": {
"node": "8" "node": "8"
} }
@ -718,6 +721,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/tailwindcss": {
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
"dev": true
},
"node_modules/toidentifier": { "node_modules/toidentifier": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",

View File

@ -11,5 +11,8 @@
}, },
"dependencies": { "dependencies": {
"express": "^4.17.1" "express": "^4.17.1"
},
"devDependencies": {
"tailwindcss": "^4.1.14"
} }
} }

534
script.js
View File

@ -1,25 +1,118 @@
// API ключ от WeatherAPI // API ключ от WeatherAPI
const API_KEY = '485eff906f7d473b913104046250710'; const API_KEY = '485eff906f7d473b913104046250710';
const citySelect = document.getElementById('city'); const citySelect = document.getElementById('city');
const getWeatherBtn = document.getElementById('get-weather'); const getWeatherBtn = document.getElementById('get-weather');
const timelapseContainer = document.getElementById('timelapse-container'); 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
timelapseContainer.addEventListener('wheel', (event) => { let weatherSwiper;
event.preventDefault();
timelapseContainer.scrollLeft += event.deltaY; // Глобальные переменные для графиков
}); 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}`;
}
// Автоматическая загрузка погоды при загрузке страницы // Автоматическая загрузка погоды при загрузке страницы
window.addEventListener('load', () => { document.addEventListener('DOMContentLoaded', () => {
// Отображаем текущую дату
displayCurrentDate();
// Загружаем сохраненный город из localStorage
const savedCity = localStorage.getItem('selectedCity');
if (savedCity) {
citySelect.value = savedCity;
}
// Обновляем заголовок страницы
updatePageTitle();
// Ждем загрузки Swiper
if (typeof Swiper !== 'undefined') {
getWeatherBtn.click(); getWeatherBtn.click();
} else {
// Если Swiper еще не загрузился, ждем его
const checkSwiper = setInterval(() => {
if (typeof Swiper !== 'undefined') {
clearInterval(checkSwiper);
getWeatherBtn.click();
}
}, 100);
}
}); });
getWeatherBtn.addEventListener('click', async () => { getWeatherBtn.addEventListener('click', async () => {
const city = citySelect.value; const city = citySelect.value;
const url = `https://api.weatherapi.com/v1/forecast.json?key=${API_KEY}&q=${city}&days=1&hourly=true&lang=ru`;
// Сохраняем выбранный город в 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 { try {
const response = await fetch(url); const response = await fetch(url);
@ -27,35 +120,424 @@ getWeatherBtn.addEventListener('click', async () => {
throw new Error('Ошибка сети'); throw new Error('Ошибка сети');
} }
const data = await response.json(); const data = await response.json();
displayTimelapse(data.forecast.forecastday[0].hour); // Таймлапс на сегодня
// Отображаем текущую погоду
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) { } catch (error) {
console.error('Ошибка загрузки данных:', error); console.error('Ошибка загрузки данных:', error);
alert('Не удалось загрузить данные о погоде. Проверьте API ключ.'); alert('Не удалось загрузить данные о погоде. Проверьте API ключ.');
} }
}); });
function displayTimelapse(hourlyData) { 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 = ''; timelapseContainer.innerHTML = '';
hourlyData.forEach(item => { const currentHour = new Date(localtime).getHours();
const div = document.createElement('div'); let currentSlideIndex = -1;
div.className = 'timelapse-item';
hourlyData.forEach((item, index) => {
const slide = document.createElement('div');
const hour = new Date(item.time).getHours(); const hour = new Date(item.time).getHours();
div.innerHTML = ` const isCurrent = hour === currentHour;
<h3>${hour}:00</h3> if (isCurrent) currentSlideIndex = index;
<p class="temp">${Math.round(item.temp_c)}°C</p>
<p class="desc">${item.condition.text}</p> 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'}`;
<p class="prec">Осадки: ${item.precip_mm} мм (${item.chance_of_rain}%)</p> 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(div); timelapseContainer.appendChild(slide);
}); });
// Фокус на текущем времени // Пересоздаем иконки
const currentHour = new Date().getHours(); setTimeout(() => {
const currentDiv = Array.from(timelapseContainer.children).find(div => { lucide.createIcons();
const h3 = div.querySelector('h3'); }, 100);
return parseInt(h3.textContent) === currentHour;
// Инициализируем 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
}); });
if (currentDiv) { } else {
currentDiv.scrollIntoView({ behavior: 'smooth', inline: 'center' }); 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'
}
}
});
} }

View File

@ -1,6 +1,6 @@
const express = require('express'); const express = require('express');
const app = express(); const app = express();
const port = 3000; const port = process.env.PORT || 3000;
// Обслуживание статических файлов // Обслуживание статических файлов
app.use(express.static('.')); app.use(express.static('.'));

293
style.css
View File

@ -1,293 +0,0 @@
body {
font-family: 'Inter', sans-serif;
background-color: #f8f9fb;
color: #333;
margin: 0;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
padding: 40px;
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: 2.5rem;
font-weight: 600;
color: #333;
margin: 0;
}
.city-selector {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40px;
padding: 24px;
background-color: #eef0f4;
border-radius: 12px;
}
label {
font-weight: 500;
margin-bottom: 10px;
color: #555;
}
select {
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 1rem;
background-color: white;
margin-bottom: 15px;
min-width: 200px;
}
button {
padding: 12px 24px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #2563eb;
}
.weather-info {
display: flex;
flex-direction: column;
gap: 40px;
}
.timelapse-container {
display: flex;
flex-direction: row;
overflow-x: scroll;
padding: 10px 0;
scrollbar-width: thin;
scrollbar-color: #ccc transparent;
}
.timelapse-item {
flex-shrink: 0;
width: 140px;
margin-right: 15px;
background-color: #eef0f4;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.2s ease;
}
.timelapse-item:hover {
transform: translateY(-2px);
}
.timelapse-item h3 {
margin: 0 0 8px 0;
font-size: 1.1rem;
font-weight: 600;
color: #333;
}
.timelapse-item p {
margin: 4px 0;
font-size: 0.9rem;
color: #666;
}
.timelapse-item .temp {
font-size: 1.2rem;
font-weight: 500;
color: #3b82f6;
}
.timelapse-item .desc {
font-weight: 500;
}
.timelapse-item .prec {
margin: 4px 0;
font-size: 0.8rem;
color: #777;
}
.weather-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.weather-item {
background-color: #eef0f4;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
transition: transform 0.2s ease;
}
.weather-item:hover {
transform: translateY(-2px);
}
.weather-item h3 {
margin: 0 0 8px 0;
font-size: 1.1rem;
font-weight: 600;
color: #333;
}
.weather-item p {
margin: 4px 0;
font-size: 0.9rem;
color: #666;
}
.weather-item .temp {
font-size: 1.2rem;
font-weight: 500;
color: #3b82f6;
}
.weather-item .desc {
font-weight: 500;
}
/* Медиа-запросы для адаптивности */
/* Планшеты и небольшие десктопы */
@media (min-width: 768px) {
.weather-info {
flex-direction: row;
}
.hourly-weather, .daily-weather {
flex: 1;
}
.container {
padding: 50px;
}
h1 {
font-size: 3rem;
}
}
/* Большие экраны */
@media (min-width: 1200px) {
.container {
max-width: 1600px;
padding: 60px;
}
.timelapse-item {
width: 160px;
margin-right: 20px;
padding: 24px;
}
h1 {
font-size: 3.5rem;
}
.timelapse-item h3 {
font-size: 1.2rem;
}
.timelapse-item .temp {
font-size: 1.4rem;
}
}
/* Сверхбольшие экраны */
@media (min-width: 1440px) {
.container {
max-width: 1800px;
padding: 80px;
}
.timelapse-item {
width: 180px;
margin-right: 25px;
padding: 28px;
}
h1 {
font-size: 4rem;
}
.timelapse-item h3 {
font-size: 1.3rem;
margin-bottom: 12px;
}
.timelapse-item .temp {
font-size: 1.6rem;
margin: 8px 0;
}
.timelapse-item .desc {
font-size: 1rem;
margin: 6px 0;
}
.timelapse-item .prec {
font-size: 0.9rem;
margin: 6px 0;
}
}
/* Ультра-широкие экраны */
@media (min-width: 1920px) {
.container {
max-width: 2200px;
padding: 100px;
}
.timelapse-item {
width: 200px;
margin-right: 30px;
padding: 32px;
}
h1 {
font-size: 4.5rem;
}
.city-selector {
padding: 30px;
margin-bottom: 50px;
}
select {
padding: 16px 20px;
font-size: 1.1rem;
min-width: 250px;
margin-bottom: 20px;
}
button {
padding: 16px 32px;
font-size: 1.1rem;
}
}