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:
parent
dce99ec5dd
commit
096dac61aa
57
README.md
Normal file
57
README.md
Normal 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
|
||||
201
index.html
201
index.html
@ -4,29 +4,198 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Погода</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<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>">
|
||||
<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>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Погода</h1>
|
||||
<body class="bg-gradient-to-br from-blue-50 via-white to-indigo-50 min-h-screen">
|
||||
<div class="container mx-auto max-w-7xl px-4 py-8">
|
||||
<!-- Header -->
|
||||
<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>
|
||||
<div class="city-selector">
|
||||
<label for="city">Выберите город:</label>
|
||||
<select id="city">
|
||||
<option value="Novosibirsk">Новосибирск</option>
|
||||
<option value="Moscow">Москва</option>
|
||||
</select>
|
||||
<button id="get-weather">Получить погоду</button>
|
||||
|
||||
<!-- City Selector -->
|
||||
<div class="bg-white rounded-2xl shadow-xl p-8 mb-12 border border-gray-100">
|
||||
<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="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>
|
||||
<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>
|
||||
|
||||
<!-- Weather Info -->
|
||||
<div class="weather-info">
|
||||
<div class="timelapse-weather">
|
||||
<h2>Таймлапс погоды</h2>
|
||||
<div id="timelapse-container" class="timelapse-container"></div>
|
||||
<!-- Current Weather Section -->
|
||||
<div id="current-weather" class="hidden bg-white rounded-2xl shadow-xl p-8 mb-8 border border-gray-100">
|
||||
<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 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>
|
||||
<!-- Swiper JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@ -10,6 +10,9 @@
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.1.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": "8"
|
||||
}
|
||||
@ -718,6 +721,12 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
||||
@ -11,5 +11,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^4.1.14"
|
||||
}
|
||||
}
|
||||
540
script.js
540
script.js
@ -1,25 +1,118 @@
|
||||
// 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');
|
||||
|
||||
// Добавляем скроллинг колесиком мыши для таймлапса
|
||||
timelapseContainer.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
timelapseContainer.scrollLeft += event.deltaY;
|
||||
});
|
||||
// Глобальная переменная для 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}`;
|
||||
}
|
||||
|
||||
// Автоматическая загрузка погоды при загрузке страницы
|
||||
window.addEventListener('load', () => {
|
||||
getWeatherBtn.click();
|
||||
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;
|
||||
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 {
|
||||
const response = await fetch(url);
|
||||
@ -27,35 +120,424 @@ getWeatherBtn.addEventListener('click', async () => {
|
||||
throw new Error('Ошибка сети');
|
||||
}
|
||||
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) {
|
||||
console.error('Ошибка загрузки данных:', error);
|
||||
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 = '';
|
||||
hourlyData.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'timelapse-item';
|
||||
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();
|
||||
div.innerHTML = `
|
||||
<h3>${hour}:00</h3>
|
||||
<p class="temp">${Math.round(item.temp_c)}°C</p>
|
||||
<p class="desc">${item.condition.text}</p>
|
||||
<p class="prec">Осадки: ${item.precip_mm} мм (${item.chance_of_rain}%)</p>
|
||||
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(div);
|
||||
timelapseContainer.appendChild(slide);
|
||||
});
|
||||
|
||||
// Фокус на текущем времени
|
||||
const currentHour = new Date().getHours();
|
||||
const currentDiv = Array.from(timelapseContainer.children).find(div => {
|
||||
const h3 = div.querySelector('h3');
|
||||
return parseInt(h3.textContent) === currentHour;
|
||||
});
|
||||
if (currentDiv) {
|
||||
currentDiv.scrollIntoView({ behavior: 'smooth', inline: 'center' });
|
||||
}
|
||||
// Пересоздаем иконки
|
||||
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'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Обслуживание статических файлов
|
||||
app.use(express.static('.'));
|
||||
|
||||
293
style.css
293
style.css
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user