Last active
October 27, 2025 18:38
-
-
Save sunmeat/c2fee1abc49277b19c12374455b9ea7c to your computer and use it in GitHub Desktop.
useReducer - прогноз погоди
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}¤t=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}¤t=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