|
19 | 19 | * - [ ] Add geolocation: auto-detect user city (with permission) |
20 | 20 | * - [ ] Extract API call into /src/services/weather.js and add caching |
21 | 21 | */ |
22 | | -import { useEffect, useState } from 'react'; |
23 | | -import Loading from '../components/Loading.jsx'; |
24 | | -import ErrorMessage from '../components/ErrorMessage.jsx'; |
25 | | -import Card from '../components/Card.jsx'; |
| 22 | +import { useEffect, useState } from "react"; |
| 23 | +import Loading from "../components/Loading.jsx"; |
| 24 | +import ErrorMessage from "../components/ErrorMessage.jsx"; |
| 25 | +import Card from "../components/Card.jsx"; |
26 | 26 |
|
27 | 27 | export default function Weather() { |
28 | | - const [city, setCity] = useState('London'); |
| 28 | + const [city, setCity] = useState(""); |
29 | 29 | const [data, setData] = useState(null); |
30 | 30 | const [loading, setLoading] = useState(false); |
31 | 31 | const [error, setError] = useState(null); |
32 | | - const [unit, setUnit] = useState('C'); // °C by default |
| 32 | + const [unit, setUnit] = useState("C"); // °C by default |
| 33 | + const [isLocAllowed, setIsLocAllowed] = useState(null); |
| 34 | + const [isRequestingLoc, setIsRequestingLoc] = useState(false); |
33 | 35 |
|
34 | 36 | useEffect(() => { |
35 | | - fetchWeather(city); |
36 | | - // eslint-disable-next-line react-hooks/exhaustive-deps |
| 37 | + const storedCity = localStorage.getItem("current_city"); |
| 38 | + if (storedCity) { |
| 39 | + setIsLocAllowed(true); |
| 40 | + setCity(JSON.parse(storedCity)); |
| 41 | + } else if (navigator.geolocation) { |
| 42 | + requestLocation(); |
| 43 | + } else { |
| 44 | + setIsLocAllowed(false); |
| 45 | + setError( |
| 46 | + "Your browser does not support location detection. Please enter city manually." |
| 47 | + ); |
| 48 | + setCity("London"); |
| 49 | + } |
37 | 50 | }, []); |
38 | 51 |
|
| 52 | + useEffect(() => { |
| 53 | + if (city) { |
| 54 | + fetchWeather(city); |
| 55 | + } |
| 56 | + }, [city]); |
| 57 | + |
| 58 | + async function getCurrentCity(lat, lon) { |
| 59 | + const APIkey = import.meta.env.VITE_WEATHER_API_KEY; |
| 60 | + try { |
| 61 | + const res = await fetch( |
| 62 | + `https://api.openweathermap.org/geo/1.0/reverse?lat=${lat}&lon=${lon}&appid=${APIkey}` |
| 63 | + ); |
| 64 | + const data = await res.json(); |
| 65 | + if (data && data.length > 0 && data[0].name) { |
| 66 | + setCity(data[0].name); |
| 67 | + setError(null); |
| 68 | + setIsLocAllowed(true); |
| 69 | + localStorage.setItem("current_city", JSON.stringify(data[0].name)); |
| 70 | + } else { |
| 71 | + setCity("London"); |
| 72 | + setError("Could not detect city from location."); |
| 73 | + setIsLocAllowed(false); |
| 74 | + } |
| 75 | + } catch (err) { |
| 76 | + console.log(err); |
| 77 | + setCity("London"); |
| 78 | + setError(err.message); |
| 79 | + setIsLocAllowed(false); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + function requestLocation() { |
| 84 | + setIsRequestingLoc(true); |
| 85 | + navigator.geolocation.getCurrentPosition( |
| 86 | + async function onSuccess(position) { |
| 87 | + await getCurrentCity( |
| 88 | + position.coords.latitude, |
| 89 | + position.coords.longitude |
| 90 | + ); |
| 91 | + setIsRequestingLoc(false); |
| 92 | + }, |
| 93 | + |
| 94 | + function onError(err) { |
| 95 | + console.log("Error", err); |
| 96 | + setIsLocAllowed(false); |
| 97 | + setError( |
| 98 | + "Location is blocked. Please enable location in your browser settings to detect automatically." |
| 99 | + ); |
| 100 | + setCity("London"); |
| 101 | + setIsRequestingLoc(false); |
| 102 | + } |
| 103 | + ); |
| 104 | + } |
39 | 105 | async function fetchWeather(c) { |
40 | 106 | try { |
41 | | - setLoading(true); setError(null); |
42 | | - const res = await fetch(`https://wttr.in/${encodeURIComponent(c)}?format=j1`); |
43 | | - if (!res.ok) throw new Error('Failed to fetch'); |
| 107 | + setLoading(true); |
| 108 | + setError(null); |
| 109 | + const res = await fetch( |
| 110 | + `https://wttr.in/${encodeURIComponent(c)}?format=j1` |
| 111 | + ); |
| 112 | + if (!res.ok) throw new Error("Failed to fetch"); |
44 | 113 | const json = await res.json(); |
| 114 | + |
45 | 115 | setData(json); |
46 | 116 | } catch (e) { |
47 | | - setError(e); |
| 117 | + setError(e.message); |
48 | 118 | } finally { |
49 | 119 | setLoading(false); |
50 | 120 | } |
51 | 121 | } |
52 | 122 |
|
53 | 123 | const current = data?.current_condition?.[0]; |
54 | | - const forecast = data?.weather?.slice(0,3) || []; |
| 124 | + const forecast = data?.weather?.slice(0, 3) || []; |
55 | 125 |
|
56 | 126 | // Helper to convert °C to °F |
57 | | - const displayTemp = (c) => unit === 'C' ? c : Math.round((c * 9/5) + 32); |
| 127 | + const displayTemp = (c) => (unit === "C" ? c : Math.round((c * 9) / 5 + 32)); |
58 | 128 |
|
59 | 129 | return ( |
60 | 130 | <div> |
61 | 131 | <h2>Weather Dashboard</h2> |
62 | | - <form onSubmit={e => { e.preventDefault(); fetchWeather(city); }} className="inline-form"> |
63 | | - <input value={city} onChange={e => setCity(e.target.value)} placeholder="Enter city" /> |
64 | | - <button type="submit">Fetch</button> |
| 132 | + <form |
| 133 | + onSubmit={(e) => { |
| 134 | + e.preventDefault(); |
| 135 | + fetchWeather(city); |
| 136 | + }} |
| 137 | + className="inline-form" |
| 138 | + > |
| 139 | + <input |
| 140 | + value={city} |
| 141 | + onChange={(e) => setCity(e.target.value)} |
| 142 | + placeholder="Enter city" |
| 143 | + /> |
| 144 | + <button type="submit" disabled={isRequestingLoc}> |
| 145 | + Fetch |
| 146 | + </button> |
| 147 | + <button |
| 148 | + type="button" |
| 149 | + disabled={isRequestingLoc} |
| 150 | + onClick={() => requestLocation()} |
| 151 | + > |
| 152 | + {isLocAllowed |
| 153 | + ? isRequestingLoc |
| 154 | + ? "Updating..." |
| 155 | + : "Update location" |
| 156 | + : isRequestingLoc |
| 157 | + ? "Detecting..." |
| 158 | + : "Detect my location"} |
| 159 | + </button> |
65 | 160 | </form> |
66 | 161 |
|
67 | 162 | {/* Toggle button */} |
68 | | - <div style={{ margin: '10px 0' }}> |
69 | | - <button onClick={() => setUnit(unit === 'C' ? 'F' : 'C')}> |
70 | | - Switch to °{unit === 'C' ? 'F' : 'C'} |
| 163 | + <div style={{ margin: "10px 0" }}> |
| 164 | + <button onClick={() => setUnit(unit === "C" ? "F" : "C")}> |
| 165 | + Switch to °{unit === "C" ? "F" : "C"} |
71 | 166 | </button> |
72 | 167 | </div> |
73 | 168 |
|
74 | 169 | {loading && <Loading />} |
75 | | - <ErrorMessage error={error} /> |
| 170 | + {error && <ErrorMessage error={error} />} |
76 | 171 |
|
77 | 172 | {current && ( |
78 | | - <Card title={`Current in ${city}`}> |
79 | | - <p>Temperature: {displayTemp(Number(current.temp_C))}°{unit}</p> |
| 173 | + <Card title={`Current in ${city}`}> |
| 174 | + <p> |
| 175 | + Temperature: {displayTemp(Number(current.temp_C))}°{unit} |
| 176 | + </p> |
80 | 177 | <p>Humidity: {current.humidity}%</p> |
81 | 178 | <p>Desc: {current.weatherDesc?.[0]?.value}</p> |
82 | 179 | </Card> |
83 | 180 | )} |
84 | 181 |
|
85 | 182 | <div className="grid"> |
86 | | - {forecast.map(day => ( |
| 183 | + {forecast.map((day) => ( |
87 | 184 | <Card key={day.date} title={day.date}> |
88 | | - <p>Avg Temp: {displayTemp(Number(day.avgtempC))}°{unit}</p> |
| 185 | + <p> |
| 186 | + Avg Temp: {displayTemp(Number(day.avgtempC))}°{unit} |
| 187 | + </p> |
89 | 188 | <p>Sunrise: {day.astronomy?.[0]?.sunrise}</p> |
90 | 189 | </Card> |
91 | 190 | ))} |
92 | 191 | </div> |
93 | 192 | </div> |
94 | 193 | ); |
95 | 194 | } |
96 | | - |
|
0 commit comments