|
14 | 14 | * - [x] Persist last searched city (localStorage) |
15 | 15 | * - [x] Add error retry button component |
16 | 16 | * - [ ] Add favorites list (pin cities) |
| 17 | + * - [x] Optimize API usage by adding debounced search delay |
17 | 18 | * Advanced: |
18 | 19 | * - [ ] Hourly forecast visualization (line / area chart) |
19 | 20 | * - [x] Animate background transitions |
@@ -65,7 +66,10 @@ function renderWeatherAnimation(variant) { |
65 | 66 | <> |
66 | 67 | <svg className="cloud-svg cloud--left" viewBox="0 0 220 80" aria-hidden> |
67 | 68 | <g filter="url(#cloudBlur)"> |
68 | | - <path className="cloud-shape" d="M20 50 C20 34 42 22 62 26 C70 16 92 12 110 22 C130 8 160 12 170 28 C196 30 206 44 190 54 L30 60 C22 60 20 54 20 50 Z" /> |
| 69 | + <path |
| 70 | + className="cloud-shape" |
| 71 | + d="M20 50 C20 34 42 22 62 26 C70 16 92 12 110 22 C130 8 160 12 170 28 C196 30 206 44 190 54 L30 60 C22 60 20 54 20 50 Z" |
| 72 | + /> |
69 | 73 | </g> |
70 | 74 | <defs> |
71 | 75 | <filter id="cloudBlur" x="-20%" y="-20%" width="140%" height="140%"> |
@@ -105,7 +109,7 @@ function renderWeatherAnimation(variant) { |
105 | 109 | left: `${(i / 12) * 100}%`, |
106 | 110 | animationDelay: `${(i % 6) * 0.4}s`, |
107 | 111 | "--dur": `${10 + (i % 6)}s`, |
108 | | - "--drift": `${(i % 2 === 0 ? -40 : 40)}px`, |
| 112 | + "--drift": `${i % 2 === 0 ? -40 : 40}px`, |
109 | 113 | width: `${8 + (i % 3) * 4}px`, |
110 | 114 | height: `${8 + (i % 3) * 4}px`, |
111 | 115 | }} |
@@ -145,10 +149,20 @@ export default function Weather() { |
145 | 149 | const [activeBg, setActiveBg] = useState("default"); |
146 | 150 | const [prevBg, setPrevBg] = useState(null); |
147 | 151 |
|
| 152 | + // Fetch data initially |
148 | 153 | useEffect(() => { |
149 | 154 | fetchWeather(city); |
150 | 155 | }, []); |
151 | 156 |
|
| 157 | + // ✅ Debounced search effect |
| 158 | + useEffect(() => { |
| 159 | + if (!city.trim()) return; |
| 160 | + const handler = setTimeout(() => { |
| 161 | + fetchWeather(city); |
| 162 | + }, 800); // delay in ms |
| 163 | + return () => clearTimeout(handler); |
| 164 | + }, [city]); |
| 165 | + |
152 | 166 | async function fetchWeather(c) { |
153 | 167 | try { |
154 | 168 | setLoading(true); |
@@ -256,10 +270,7 @@ export default function Weather() { |
256 | 270 |
|
257 | 271 | {loading && <Loading />} |
258 | 272 | {error && ( |
259 | | - <ErrorMessage |
260 | | - message={error.message} |
261 | | - onRetry={() => fetchWeather(city)} |
262 | | - /> |
| 273 | + <ErrorMessage message={error.message} onRetry={() => fetchWeather(city)} /> |
263 | 274 | )} |
264 | 275 |
|
265 | 276 | {data && !loading && ( |
@@ -290,36 +301,23 @@ export default function Weather() { |
290 | 301 |
|
291 | 302 | {/* 3-Day Forecast */} |
292 | 303 | {forecast.map((day, i) => { |
293 | | - const condition = |
294 | | - day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; |
| 304 | + const condition = day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; |
295 | 305 | const badge = getBadgeStyle(condition); |
296 | 306 |
|
297 | 307 | return ( |
298 | 308 | <Card key={i} title={i === 0 ? "Today" : `Day ${i + 1}`}> |
299 | | - {day.hourly?.[0] && |
300 | | - getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( |
301 | | - <div style={{ marginTop: 8 }}> |
302 | | - <img |
303 | | - src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)} |
304 | | - alt={ |
305 | | - day.hourly?.[0]?.weatherDesc?.[0]?.value || |
306 | | - "forecast icon" |
307 | | - } |
308 | | - style={{ width: 40, height: 40, objectFit: "contain" }} |
309 | | - onError={(e) => |
310 | | - (e.currentTarget.style.display = "none") |
311 | | - } |
312 | | - /> |
313 | | - </div> |
314 | | - )} |
| 309 | + {day.hourly?.[0] && getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( |
| 310 | + <div style={{ marginTop: 8 }}> |
| 311 | + <img |
| 312 | + src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)} |
| 313 | + alt={day.hourly?.[0]?.weatherDesc?.[0]?.value || "forecast icon"} |
| 314 | + style={{ width: 40, height: 40, objectFit: "contain" }} |
| 315 | + onError={(e) => (e.currentTarget.style.display = "none")} |
| 316 | + /> |
| 317 | + </div> |
| 318 | + )} |
315 | 319 |
|
316 | | - <div |
317 | | - style={{ |
318 | | - display: "flex", |
319 | | - gap: "8px", |
320 | | - marginTop: "17px", |
321 | | - }} |
322 | | - > |
| 320 | + <div style={{ display: "flex", gap: "8px", marginTop: "17px" }}> |
323 | 321 | <strong>Avg Temp:</strong>{" "} |
324 | 322 | {displayTemp(Number(day.avgtempC))}°{unit} |
325 | 323 | <div |
|
0 commit comments