|
8 | 8 | * - [ ] Add loading skeleton instead of plain text |
9 | 9 | * - [ ] Style forecast cards with condition color badges |
10 | 10 | * Medium: |
11 | | - * - [ ] Dynamic background / gradient based on condition (sunny, rain, snow) |
12 | | - * - [ ] Input debounced search (on stop typing) |
13 | | - * - [ ] Persist last searched city (localStorage) |
14 | | - * - [ ] Add error retry button component |
15 | | - * - [ ] Add favorites list (pin cities) |
| 11 | + * - [x] Dynamic background / gradient based on condition (sunny, rain, snow) |
| 12 | + * - [x] Input debounced search (on stop typing) |
| 13 | + * - [x] Persist last searched city (localStorage) |
| 14 | + * - [x] Add error retry button component |
| 15 | + * - [ ] Add favorites list (pin cities) |
| 16 | + * - [x] Optimize API usage by adding debounced search delay |
16 | 17 | * Advanced: |
17 | 18 | * - [ ] Hourly forecast visualization (line / area chart) |
18 | 19 | * - [ ] Animate background transitions |
@@ -239,6 +240,15 @@ export default function Weather() { |
239 | 240 | return null; |
240 | 241 | } |
241 | 242 |
|
| 243 | + // ✅ Debounced search effect |
| 244 | + useEffect(() => { |
| 245 | + if (!city.trim()) return; |
| 246 | + const handler = setTimeout(() => { |
| 247 | + fetchWeather(city); |
| 248 | + }, 800); // delay in ms |
| 249 | + return () => clearTimeout(handler); |
| 250 | + }, [city]); |
| 251 | + |
242 | 252 | async function fetchWeather(c) { |
243 | 253 | try { |
244 | 254 | setLoading(true); |
@@ -385,10 +395,7 @@ export default function Weather() { |
385 | 395 |
|
386 | 396 | {loading && <Loading />} |
387 | 397 | {error && ( |
388 | | - <ErrorMessage |
389 | | - message={error.message} |
390 | | - onRetry={() => fetchWeather(city)} |
391 | | - /> |
| 398 | + <ErrorMessage message={error.message} onRetry={() => fetchWeather(city)} /> |
392 | 399 | )} |
393 | 400 |
|
394 | 401 | {data && !loading && ( |
@@ -427,40 +434,23 @@ export default function Weather() { |
427 | 434 |
|
428 | 435 | {/* 3-Day Forecast */} |
429 | 436 | {forecast.map((day, i) => { |
430 | | - const condition = |
431 | | - day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; |
| 437 | + const condition = day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; |
432 | 438 | const badge = getBadgeStyle(condition); |
433 | 439 |
|
434 | 440 | return ( |
435 | 441 | <Card key={i} title={i === 0 ? "Today" : `Day ${i + 1}`}> |
436 | | - {day.hourly?.[0] && |
437 | | - getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( |
438 | | - <div style={{ marginTop: 8 }}> |
439 | | - <img |
440 | | - src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)} |
441 | | - alt={ |
442 | | - day.hourly?.[0]?.weatherDesc?.[0]?.value || |
443 | | - "forecast icon" |
444 | | - } |
445 | | - style={{ |
446 | | - width: 40, |
447 | | - height: 40, |
448 | | - objectFit: "contain", |
449 | | - }} |
450 | | - onError={(e) => |
451 | | - (e.currentTarget.style.display = "none") |
452 | | - } |
453 | | - /> |
454 | | - </div> |
455 | | - )} |
456 | | - |
457 | | - <div |
458 | | - style={{ |
459 | | - display: "flex", |
460 | | - gap: "8px", |
461 | | - marginTop: "17px", |
462 | | - }} |
463 | | - > |
| 442 | + {day.hourly?.[0] && getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( |
| 443 | + <div style={{ marginTop: 8 }}> |
| 444 | + <img |
| 445 | + src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)} |
| 446 | + alt={day.hourly?.[0]?.weatherDesc?.[0]?.value || "forecast icon"} |
| 447 | + style={{ width: 40, height: 40, objectFit: "contain" }} |
| 448 | + onError={(e) => (e.currentTarget.style.display = "none")} |
| 449 | + /> |
| 450 | + </div> |
| 451 | + )} |
| 452 | + |
| 453 | + <div style={{ display: "flex", gap: "8px", marginTop: "17px" }}> |
464 | 454 | <strong>Avg Temp:</strong>{" "} |
465 | 455 | {displayTemp(Number(day.avgtempC))}°{unit} |
466 | 456 | <div |
|
0 commit comments