Skip to content

Commit f9900e5

Browse files
Merge branch 'main' into feat/weather-animated-background
2 parents 82161b4 + a47a146 commit f9900e5

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed

src/pages/Weather.jsx

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,34 @@ export default function Weather() {
296296
// Helper to convert °C to °F
297297
const displayTemp = (c) => (unit === "C" ? c : Math.round((c * 9) / 5 + 32));
298298

299+
// Normalize icon URL returned by wttr.in (they sometimes return // links)
300+
const getIconUrl = (iconArrOrStr) => {
301+
if (!iconArrOrStr) return null;
302+
303+
// If API returned an array like [{ value: "//..." }]
304+
if (Array.isArray(iconArrOrStr)) {
305+
const url = iconArrOrStr?.[0]?.value;
306+
if (!url) return null;
307+
return url.startsWith("//") ? `https:${url}` : url;
308+
}
309+
310+
// If API returned a plain string (rare) or already a URL
311+
if (typeof iconArrOrStr === "string") {
312+
return iconArrOrStr.startsWith("//")
313+
? `https:${iconArrOrStr}`
314+
: iconArrOrStr;
315+
}
316+
317+
return null;
318+
};
319+
320+
// Format wttr.in hourly time values like "0", "300", "1500" -> "00:00", "03:00", "15:00"
321+
const formatWttTime = (t) => {
322+
if (t == null) return "";
323+
const s = String(t).padStart(4, "0");
324+
return `${s.slice(0, 2)}:${s.slice(2)}`;
325+
};
326+
299327
const getBadgeStyle = (condition) => {
300328
if (!condition) return { color: "#E0E0E0", label: "Clear 🌤️" };
301329

@@ -383,6 +411,164 @@ export default function Weather() {
383411
Switch to °{unit === "C" ? "F" : "C"}
384412
</button>
385413
</div>
414+
{loading && <Loading />}
415+
{error && (
416+
<ErrorMessage
417+
message={error.message}
418+
onRetry={() => fetchWeather(city)}
419+
/>
420+
)}
421+
422+
{data && !loading && (
423+
<div className="dashboard-grid">
424+
{/* Current Weather */}
425+
<Card title="Current Weather" size="large">
426+
<h2>{data.nearest_area?.[0]?.areaName?.[0]?.value || city}</h2>
427+
<p style={{ display: "flex", alignItems: "center", gap: "12px" }}>
428+
{current && getIconUrl(current.weatherIconUrl) && (
429+
<img
430+
src={getIconUrl(current.weatherIconUrl)}
431+
alt={current.weatherDesc?.[0]?.value || "weather icon"}
432+
style={{ width: 48, height: 48, objectFit: "contain" }}
433+
/>
434+
)}
435+
<span>
436+
<strong>Temperature:</strong>{" "}
437+
{displayTemp(Number(current.temp_C))}°{unit}
438+
</span>
439+
</p>
440+
<p>
441+
<strong>Humidity:</strong> {current.humidity}%
442+
</p>
443+
<p>
444+
<strong>Desc:</strong> {current.weatherDesc?.[0]?.value}
445+
</p>
446+
</Card>
447+
448+
{/* 3-Day Forecast */}
449+
{forecast.map((day, i) => {
450+
const condition =
451+
day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear";
452+
const badge = getBadgeStyle(condition);
453+
454+
return (
455+
<Card key={i} title={i === 0 ? "Today" : `Day ${i + 1}`}>
456+
{/* Badge Section */}
457+
{/* Forecast icon (use first hourly entry icon) */}
458+
{day.hourly?.[0] &&
459+
getIconUrl(day.hourly?.[0]?.weatherIconUrl) && (
460+
<div style={{ marginTop: 8 }}>
461+
<img
462+
src={getIconUrl(day.hourly?.[0]?.weatherIconUrl)}
463+
alt={
464+
day.hourly?.[0]?.weatherDesc?.[0]?.value ||
465+
"forecast icon"
466+
}
467+
style={{ width: 40, height: 40, objectFit: "contain" }}
468+
onError={(e) =>
469+
(e.currentTarget.style.display = "none")
470+
}
471+
/>
472+
</div>
473+
)}
474+
475+
{/* Full-day hourly timeline (0:00 - 23:00) */}
476+
{day.hourly && (
477+
<div
478+
style={{
479+
display: "block",
480+
marginTop: 8,
481+
}}
482+
>
483+
<div
484+
style={{
485+
display: "flex",
486+
gap: 12,
487+
overflowX: "auto",
488+
padding: "8px 4px",
489+
WebkitOverflowScrolling: "touch",
490+
}}
491+
>
492+
{day.hourly.map((h, idx) => {
493+
const icon = getIconUrl(h.weatherIconUrl);
494+
const t = h.time ?? h.Time ?? "";
495+
const temp =
496+
h.tempC ?? h.temp_C ?? h.tempC ?? h.tempF ?? "";
497+
498+
return (
499+
<div
500+
key={idx}
501+
style={{
502+
minWidth: 72,
503+
padding: 6,
504+
borderRadius: 6,
505+
background: "rgba(0,0,0,0.03)",
506+
display: "flex",
507+
flexDirection: "column",
508+
alignItems: "center",
509+
justifyContent: "center",
510+
fontSize: 12,
511+
}}
512+
>
513+
{icon ? (
514+
<img
515+
src={icon}
516+
alt={h.weatherDesc?.[0]?.value || "hour icon"}
517+
style={{
518+
width: 36,
519+
height: 36,
520+
objectFit: "contain",
521+
}}
522+
loading="lazy"
523+
onError={(e) =>
524+
(e.currentTarget.style.display = "none")
525+
}
526+
/>
527+
) : (
528+
<div style={{ fontSize: 18 }}>🌤️</div>
529+
)}
530+
<div style={{ marginTop: 6 }}>
531+
{formatWttTime(t)}
532+
</div>
533+
<div style={{ fontWeight: 700 }}>
534+
{displayTemp(Number(temp))}°{unit}
535+
</div>
536+
</div>
537+
);
538+
})}
539+
</div>
540+
</div>
541+
)}
542+
543+
<div style={
544+
{display:"flex",gap:"8px", marginTop:"17px"}
545+
}>
546+
<strong>Avg Temp:</strong> {displayTemp(Number(day.avgtempC))}
547+
°{unit}
548+
<div
549+
style={{
550+
backgroundColor: badge.color,
551+
borderRadius: "8px",
552+
padding: "4px 8px",
553+
display: "inline-block",
554+
fontSize: "12px",
555+
fontWeight: "bold",
556+
marginBottom: "8px",
557+
color: "#333",
558+
}}
559+
>
560+
{badge.label}
561+
</div>
562+
</div>
563+
<p>
564+
<strong>Sunrise:</strong> {day.astronomy?.[0]?.sunrise}
565+
</p>
566+
<p>
567+
<strong>Sunset:</strong> {day.astronomy?.[0]?.sunset}
568+
</p>
569+
</Card>
570+
);
571+
})}
386572
</div>
387573

388574
{loading && <Loading />}

0 commit comments

Comments
 (0)