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 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">
|
||||||
<option value="Moscow">Москва</option>
|
<div class="flex items-center gap-3 text-gray-700">
|
||||||
</select>
|
<i data-lucide="map-pin" class="w-5 h-5"></i>
|
||||||
<button id="get-weather">Получить погоду</button>
|
<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>
|
</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 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>
|
</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
9
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -11,5 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^4.1.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
540
script.js
540
script.js
@ -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', () => {
|
||||||
getWeatherBtn.click();
|
// Отображаем текущую дату
|
||||||
|
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 () => {
|
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 если он еще не создан
|
||||||
if (currentDiv) {
|
setTimeout(() => {
|
||||||
currentDiv.scrollIntoView({ behavior: 'smooth', inline: 'center' });
|
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 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
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