From a39305ab7bb755d5675b8259731d4787c2af99ed Mon Sep 17 00:00:00 2001 From: Priya Nandwani Date: Tue, 14 Oct 2025 01:12:37 +0530 Subject: [PATCH 1/3] Added loading skeletons and aria labels for weather dashboard --- src/App.jsx | 2 +- src/components/Skeleton.jsx | 9 ++ src/pages/Weather.jsx | 114 ++++++++++++----- src/styles.css | 245 ++++++++++++++++++++++++++++++------ 4 files changed, 299 insertions(+), 71 deletions(-) create mode 100644 src/components/Skeleton.jsx diff --git a/src/App.jsx b/src/App.jsx index 89814b6..0fa4e90 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -65,7 +65,7 @@ export default function App() { } /> } /> } /> - } /> + } /> diff --git a/src/components/Skeleton.jsx b/src/components/Skeleton.jsx new file mode 100644 index 0000000..a2d4b16 --- /dev/null +++ b/src/components/Skeleton.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const Skeleton = ({width, height}) => { + return ( + + ) +} + +export default Skeleton \ No newline at end of file diff --git a/src/pages/Weather.jsx b/src/pages/Weather.jsx index 18c3b43..8339410 100644 --- a/src/pages/Weather.jsx +++ b/src/pages/Weather.jsx @@ -19,28 +19,32 @@ * - [ ] Add geolocation: auto-detect user city (with permission) * - [ ] Extract API call into /src/services/weather.js and add caching */ -import { useEffect, useState } from 'react'; -import Loading from '../components/Loading.jsx'; -import ErrorMessage from '../components/ErrorMessage.jsx'; -import Card from '../components/Card.jsx'; + +import { useEffect, useState } from "react"; +import ErrorMessage from "../components/ErrorMessage.jsx"; +import Card from "../components/Card.jsx"; +import Skeleton from "../components/Skeleton.jsx"; export default function Weather() { - const [city, setCity] = useState('London'); + const [city, setCity] = useState("London"); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [unit, setUnit] = useState('C'); // °C by default + const [unit, setUnit] = useState("C"); // °C by default useEffect(() => { fetchWeather(city); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); async function fetchWeather(c) { try { - setLoading(true); setError(null); - const res = await fetch(`https://wttr.in/${encodeURIComponent(c)}?format=j1`); - if (!res.ok) throw new Error('Failed to fetch'); + setLoading(true); + setError(null); + const res = await fetch( + `https://wttr.in/${encodeURIComponent(c)}?format=j1` + ); + if (!res.ok) throw new Error("Failed to fetch"); const json = await res.json(); setData(json); } catch (e) { @@ -51,46 +55,88 @@ export default function Weather() { } const current = data?.current_condition?.[0]; - const forecast = data?.weather?.slice(0,3) || []; + const forecast = data?.weather?.slice(0, 3) || []; // Helper to convert °C to °F - const displayTemp = (c) => unit === 'C' ? c : Math.round((c * 9/5) + 32); + const displayTemp = (c) => (unit === "C" ? c : Math.round((c * 9) / 5 + 32)); return (

Weather Dashboard

-
{ e.preventDefault(); fetchWeather(city); }} className="inline-form"> - setCity(e.target.value)} placeholder="Enter city" /> - + + { + e.preventDefault(); + fetchWeather(city); + }} + className="inline-form" + aria-label="City search form" + > + setCity(e.target.value)} + placeholder="Enter city" + aria-label="Enter city name" + /> +
{/* Toggle button */} -
-
- {loading && } - {current && ( - -

Temperature: {displayTemp(Number(current.temp_C))}°{unit}

-

Humidity: {current.humidity}%

-

Desc: {current.weatherDesc?.[0]?.value}

-
- )} + +
+ Temperature:{" "} + {loading || !current?.temp_C ? : `${displayTemp(Number(current.temp_C))}°${unit}`} +
+ +
+ Humidity:{" "} + {loading || !current?.humidity ? : `${current.humidity}%`} +
-
- {forecast.map(day => ( - -

Avg Temp: {displayTemp(Number(day.avgtempC))}°{unit}

-

Sunrise: {day.astronomy?.[0]?.sunrise}

-
- ))} +
+ Desc:{" "} + {loading || !current?.weatherDesc?.[0]?.value ? : current.weatherDesc[0].value} +
+ + +
+ {(loading ? Array(3).fill({}) : forecast).map((day, index) => { + const isSkeleton = loading || !day.avgtempC; + return ( + : day.date} + aria-label={loading ? "Forecast data loading" : `Forecast for ${day.date}`} + > +
+ Avg Temp: {isSkeleton ? : `${displayTemp(Number(day.avgtempC))}°${unit}`} +
+ +
+ Sunrise: {isSkeleton ? : day.astronomy?.[0]?.sunrise} +
+
+ ); + })}
); } - diff --git a/src/styles.css b/src/styles.css index d837b58..70debcc 100644 --- a/src/styles.css +++ b/src/styles.css @@ -9,7 +9,9 @@ --radius: 8px; --transition: 0.25s ease; } -.theme-dark, [data-theme="dark"], .theme-dark :root { +.theme-dark, +[data-theme="dark"], +.theme-dark :root { --bg: #0f172a; --bg-alt: #1e293b; --text: #f1f5f9; @@ -17,56 +19,227 @@ --primary: #3b82f6; --danger: #f87171; } -body, .app { background: var(--bg); color: var(--text); font-family: system-ui, Arial, sans-serif; margin:0; min-height:100vh; } -.container { padding: 1rem clamp(1rem, 2vw, 2rem); } -a { color: var(--primary); text-decoration:none; } -a:hover { text-decoration:underline; } +body, +.app { + background: var(--bg); + color: var(--text); + font-family: system-ui, Arial, sans-serif; + margin: 0; + min-height: 100vh; +} +.container { + padding: 1rem clamp(1rem, 2vw, 2rem); +} +a { + color: var(--primary); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} /* Navbar */ -.navbar { display:flex; align-items:center; gap:1rem; padding:0.75rem 1rem; background:var(--bg-alt); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:10; } -.navbar .logo { margin:0; font-size:1.1rem; } -.navbar ul { list-style:none; display:flex; gap:0.75rem; margin:0; padding:0; } -.navbar a.active { font-weight:600; } -.nav-toggle { display:none; } -@media (max-width: 900px){ - .navbar ul { flex-direction:column; position:absolute; left:0; right:0; top:56px; background:var(--bg-alt); padding:1rem; display:none; } - body.nav-open .navbar ul { display:flex; } - .nav-toggle { display:block; background:none; border:1px solid var(--border); padding:0.5rem 0.75rem; border-radius:var(--radius); } +.navbar { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 1rem; + background: var(--bg-alt); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} +.navbar .logo { + margin: 0; + font-size: 1.1rem; +} +.navbar ul { + list-style: none; + display: flex; + gap: 0.75rem; + margin: 0; + padding: 0; +} +.navbar a.active { + font-weight: 600; +} +.nav-toggle { + display: none; +} +@media (max-width: 900px) { + .navbar ul { + flex-direction: column; + position: absolute; + left: 0; + right: 0; + top: 56px; + background: var(--bg-alt); + padding: 1rem; + display: none; + } + body.nav-open .navbar ul { + display: flex; + } + .nav-toggle { + display: block; + background: none; + border: 1px solid var(--border); + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + } } /* Layout */ -.grid { display:grid; gap:1rem; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); margin-top:1rem; } -.flex { display:flex; } -.gap { gap:1rem; } -.wrap { flex-wrap:wrap; } -.inline-form { display:flex; gap:0.5rem; margin:1rem 0; } +.grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + margin-top: 1rem; +} +.flex { + display: flex; +} +.gap { + gap: 1rem; +} +.wrap { + flex-wrap: wrap; +} +.inline-form { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} /* Cards */ -.card { background:var(--bg-alt); border:1px solid var(--border); border-radius:var(--radius); padding:1rem; display:flex; flex-direction:column; gap:0.5rem; box-shadow:0 1px 2px rgba(0,0,0,0.06); } -.card h3 { margin:0 0 .25rem; font-size:1rem; } -.card-footer { margin-top:auto; font-size:0.85rem; opacity:0.8; } -.chart-placeholder { background: repeating-linear-gradient(45deg, var(--bg), var(--bg) 10px, var(--bg-alt) 10px, var(--bg-alt) 20px); border:1px dashed var(--border); padding:2rem; text-align:center; border-radius:var(--radius); } +.card { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); +} +.card h3 { + margin: 0 0 0.25rem; + font-size: 1rem; +} +.card-footer { + margin-top: auto; + font-size: 0.85rem; + opacity: 0.8; +} +.chart-placeholder { + background: repeating-linear-gradient( + 45deg, + var(--bg), + var(--bg) 10px, + var(--bg-alt) 10px, + var(--bg-alt) 20px + ); + border: 1px dashed var(--border); + padding: 2rem; + text-align: center; + border-radius: var(--radius); +} /* Buttons & inputs */ -button, input, select { font: inherit; padding:0.5rem 0.75rem; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg); color:var(--text); } -button { cursor:pointer; background:var(--primary); color:#fff; border-color:var(--primary); transition:var(--transition); } -button:hover { filter:brightness(1.1); } -button:disabled { opacity:0.5; cursor:not-allowed; } -input, select { background:var(--bg-alt); } +button, +input, +select { + font: inherit; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg); + color: var(--text); +} +button { + cursor: pointer; + background: var(--primary); + color: #fff; + border-color: var(--primary); + transition: var(--transition); +} +button:hover { + filter: brightness(1.1); +} +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} +input, +select { + background: var(--bg-alt); +} /* Status */ -.loading { padding:0.5rem 0; opacity:0.8; } -.error { color:var(--danger); font-weight:600; } -.correct { background: #16a34a !important; color:#fff; } -.wrong { background:#dc2626 !important; color:#fff; } +.loading { + padding: 0.5rem 0; + opacity: 0.8; +} +.error { + color: var(--danger); + font-weight: 600; +} +.correct { + background: #16a34a !important; + color: #fff; +} +.wrong { + background: #dc2626 !important; + color: #fff; +} -img { border-radius:var(--radius); } +img { + border-radius: var(--radius); +} /* Theme switcher */ -.theme-switcher { position:fixed; bottom:1rem; right:1rem; } -.theme-switcher button { background:var(--bg-alt); color:var(--text); border:1px solid var(--border); } +.theme-switcher { + position: fixed; + bottom: 1rem; + right: 1rem; +} +.theme-switcher button { + background: var(--bg-alt); + color: var(--text); + border: 1px solid var(--border); +} /* Utilities */ -blockquote { margin:0; font-style:italic; } +blockquote { + margin: 0; + font-style: italic; +} /* TODO: Add prefers-color-scheme auto detection */ + +/* Base skeleton placeholder */ +.skeleton { + display: inline-block; + opacity: 0.7; + background-color: rgb(237, 235, 235); + animation: skeleton-loading 1s linear infinite alternate; + border-radius: 5px; + width: min-content; + padding: 2px; + padding-inline: 5px; + width: 40px; + height: 14px; + vertical-align: middle; +margin-left: 5px; + +} + +@keyframes skeleton-loading { + 0% { + background-color: hsl(200, 20%, 70%); + } + 100% { + background-color: hsl(200, 20, 95%); + } +} From 0f7112e7b94913c918fc3d16988deda64638c188 Mon Sep 17 00:00:00 2001 From: Priya Nandwani Date: Tue, 14 Oct 2025 16:03:24 +0530 Subject: [PATCH 2/3] Fix minor CSS issues and skeleton styles --- src/components/Skeleton.jsx | 6 +++--- src/pages/Weather.jsx | 10 +++++----- src/styles.css | 13 +++++-------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/components/Skeleton.jsx b/src/components/Skeleton.jsx index a2d4b16..d89e5bf 100644 --- a/src/components/Skeleton.jsx +++ b/src/components/Skeleton.jsx @@ -1,9 +1,9 @@ -import React from 'react' +import React from 'react'; const Skeleton = ({width, height}) => { return ( - + ) } -export default Skeleton \ No newline at end of file +export default Skeleton; \ No newline at end of file diff --git a/src/pages/Weather.jsx b/src/pages/Weather.jsx index 8339410..433950d 100644 --- a/src/pages/Weather.jsx +++ b/src/pages/Weather.jsx @@ -98,17 +98,17 @@ export default function Weather() {
Temperature:{" "} - {loading || !current?.temp_C ? : `${displayTemp(Number(current.temp_C))}°${unit}`} + {loading || !current?.temp_C ? : `${displayTemp(Number(current.temp_C))}°${unit}`}
Humidity:{" "} - {loading || !current?.humidity ? : `${current.humidity}%`} + {loading || !current?.humidity ? : `${current.humidity}%`}
Desc:{" "} - {loading || !current?.weatherDesc?.[0]?.value ? : current.weatherDesc[0].value} + {loading || !current?.weatherDesc?.[0]?.value ? : current.weatherDesc[0].value}
@@ -118,14 +118,14 @@ export default function Weather() { return ( : day.date} + title={loading ? : day.date} aria-label={loading ? "Forecast data loading" : `Forecast for ${day.date}`} >
- Avg Temp: {isSkeleton ? : `${displayTemp(Number(day.avgtempC))}°${unit}`} + Avg Temp: {isSkeleton ? : `${displayTemp(Number(day.avgtempC))}°${unit}`}
Date: Wed, 15 Oct 2025 15:38:32 +0530 Subject: [PATCH 3/3] Fix reviewer feedback: skeleton CSS & JSX issues --- src/pages/Weather.jsx | 6 +++--- src/styles.css | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/Weather.jsx b/src/pages/Weather.jsx index 433950d..773df1a 100644 --- a/src/pages/Weather.jsx +++ b/src/pages/Weather.jsx @@ -98,17 +98,17 @@ export default function Weather() {
Temperature:{" "} - {loading || !current?.temp_C ? : `${displayTemp(Number(current.temp_C))}°${unit}`} + {loading || !current?.temp_C ? : `${displayTemp(Number(current.temp_C))}°${unit}`}
Humidity:{" "} - {loading || !current?.humidity ? : `${current.humidity}%`} + {loading || !current?.humidity ? : `${current.humidity}%`}
Desc:{" "} - {loading || !current?.weatherDesc?.[0]?.value ? : current.weatherDesc[0].value} + {loading || !current?.weatherDesc?.[0]?.value ? : current.weatherDesc[0].value}
diff --git a/src/styles.css b/src/styles.css index 2577e71..0600342 100644 --- a/src/styles.css +++ b/src/styles.css @@ -229,7 +229,6 @@ blockquote { height: 16px; vertical-align: middle; margin-left: 5px; - } @keyframes skeleton-loading {