Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active October 27, 2025 18:38
Show Gist options
  • Save sunmeat/c2fee1abc49277b19c12374455b9ea7c to your computer and use it in GitHub Desktop.
Save sunmeat/c2fee1abc49277b19c12374455b9ea7c to your computer and use it in GitHub Desktop.
useReducer - прогноз погоди
App.jsx:
import { useReducer, useEffect, useState } from 'react';
import './App.css';
// початковий стан
const initialState = {
loading: false,
data: null,
error: null,
citiesWeather: [], // погода міст
};
// редюсер для керування станами відправлення запиту
function reducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, data: null, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload, error: null };
case 'FETCH_ERROR':
return { ...state, loading: false, data: null, error: action.payload };
case 'SET_CITIES_WEATHER':
return { ...state, citiesWeather: action.payload };
default:
return state;
}
}
const UKRAINE_CITIES = [
'Kyiv', 'Kharkiv', 'Odesa', 'Dnipro', 'Donetsk', 'Zaporizhzhia', 'Lviv', 'Kryvyi Rih',
'Mykolaiv', 'Mariupol', 'Luhansk', 'Vinnytsia', 'Kherson', 'Poltava', 'Chernihiv',
'Cherkasy', 'Zhytomyr', 'Sumy', 'Rivne', 'Ivano-Frankivsk',
];
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
const [city, setCity] = useState('Mykolaiv');
const [submittedCity, setSubmittedCity] = useState('');
// запит погоди для введеного міста
useEffect(() => {
if (submittedCity) {
dispatch({ type: 'FETCH_START' });
fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${submittedCity}&count=1`)
.then((response) => {
if (!response.ok) throw new Error('Місто не знайдено або помилка API');
return response.json();
})
.then((geoData) => {
if (!geoData.results || geoData.results.length === 0) {
throw new Error('Місто не знайдено');
}
const { latitude, longitude, name } = geoData.results[0];
return fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weather_code`
)
.then((response) => {
if (!response.ok) throw new Error('Дані про погоду недоступні');
return response.json();
})
.then((weatherData) => {
dispatch({
type: 'FETCH_SUCCESS',
payload: {
city: name,
temperature: weatherData.current.temperature_2m,
description: getWeatherDescription(weatherData.current.weather_code),
},
});
});
})
.catch((error) => {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
});
}
}, [submittedCity]);
// запит погоди для міст
useEffect(() => {
const fetchCitiesWeather = async () => {
const citiesWeatherData = await Promise.all(
UKRAINE_CITIES.map(async (city) => {
try {
const geoResponse = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${city}&count=1`
);
if (!geoResponse.ok) throw new Error('Місто не знайдено');
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
throw new Error('Місто не знайдено');
}
const { latitude, longitude, name } = geoData.results[0];
const weatherResponse = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m`
);
if (!weatherResponse.ok) throw new Error('Дані про погоду недоступні');
const weatherData = await weatherResponse.json();
return { city: name, temperature: weatherData.current.temperature_2m };
} catch (error) {
return { city, temperature: 'N/A' + error.message };
}
})
);
dispatch({ type: 'SET_CITIES_WEATHER', payload: citiesWeatherData });
};
fetchCitiesWeather();
}, []);
// перетворення weather_code в опис
const getWeatherDescription = (code) => {
const weatherCodes = {
0: 'Ясне небо',
1: 'Переважно ясно',
2: 'Мінлива хмарність',
3: 'Хмарно',
45: 'Туман',
48: 'Іній',
51: 'Легка мряка',
53: 'Помірна мряка',
55: 'Сильна мряка',
56: 'Легкий іній',
57: 'Сильний іній',
61: 'Легкий дощ',
63: 'Помірний дощ',
65: 'Сильний дощ',
66: 'Легкий крижаний дощ',
67: 'Сильний крижаний дощ',
71: 'Легкий сніг',
73: 'Помірний сніг',
75: 'Сильний сніг',
77: 'Снігові зерна',
80: 'Легкий злива',
81: 'Помірна злива',
82: 'Сильна злива',
85: 'Легкий сніговий злива',
86: 'Сильний сніговий злива',
95: 'Гроза',
96: 'Гроза з легким градом',
99: 'Гроза з сильним градом',
};
return weatherCodes[code] || 'Невідомо';
};
// обробник відправлення форми
const handleSubmit = (e) => {
e.preventDefault();
if (city.trim()) {
setSubmittedCity(city);
}
};
return (
<div className="app-container">
<h1>Прогноз погоди</h1>
<form onSubmit={handleSubmit} className="weather-form">
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Введіть назву міста"
className="city-input"
/>
<button type="submit" className="btn btn-primary">
Дізнатися погоду
</button>
</form>
<div className="weather-result">
{state.loading && <p>Завантаження...</p>}
{state.error && <p className="error">{state.error}</p>}
{state.data && (
<div className="weather-info">
<h2>{state.data.city}</h2>
<p>Температура: {state.data.temperature}°C</p>
<p>Погода: {state.data.description}</p>
</div>
)}
</div>
<div className="cities-list">
<h3>Температура в містах України</h3>
<ul>
{state.citiesWeather.map((cityWeather, index) => (
<li key={index}>
{cityWeather.city}: {cityWeather.temperature}°C
</li>
))}
</ul>
</div>
</div>
);
}
export default App;
==========================================================================================
App.css:
.app-container {
max-width: 600px;
margin: 40px auto;
padding: 20px;
background-color: #f4f7fa;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-align: center;
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
}
.weather-form {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
}
.city-input {
padding: 10px;
width: 70%;
border: 1px solid #dcdcdc;
border-radius: 5px;
font-size: 16px;
}
.city-input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.weather-result {
margin-top: 20px;
}
.weather-info {
background-color: #ffffff;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.weather-info h2 {
margin: 0 0 10px;
color: #2c3e50;
}
.weather-info p {
margin: 5px 0;
color: #34495e;
}
.error {
color: #e74c3c;
font-weight: bold;
}
.cities-list {
margin-top: 30px;
}
.cities-list h3 {
color: #2c3e50;
margin-bottom: 10px;
}
.cities-list ul {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.cities-list li {
background-color: #ffffff;
padding: 10px;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
color: #34495e;
font-size: 14px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment