diff --git a/.gitignore b/.gitignore index a490dad..d9aa0d8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist .DS_Store *.log package-lock.json +.env diff --git a/src/pages/Weather.jsx b/src/pages/Weather.jsx index 6a68d52..657ee29 100644 --- a/src/pages/Weather.jsx +++ b/src/pages/Weather.jsx @@ -2,22 +2,22 @@ * WEATHER DASHBOARD TODOs * ----------------------- * Easy: - * - [x] Extract API call into /src/services/weather.js and add caching - * - [x] Add °C / °F toggle - * - [x] Show weather icon (current + forecast) - * - [x] Show feels-like temperature & wind speed - * - [x] Add loading skeleton instead of plain text - * - [x] Style forecast cards with condition color badges + * - [ ] Add °C / °F toggle + * - [ ] Show weather icon (current + forecast) + * - [ ] Show feels-like temperature & wind speed + * - [ ] Add loading skeleton instead of plain text + * - [ ] Style forecast cards with condition color badges * Medium: - * - [x] Dynamic background / gradient based on condition (sunny, rain, snow) - * - [x] Input debounced search (on stop typing) - * - [x] Persist last searched city (localStorage) - * - [x] Add error retry button component - * - [ ] Add favorites list (pin cities) + * - [ ] Dynamic background / gradient based on condition (sunny, rain, snow) + * - [ ] Input debounced search (on stop typing) + * - [ ] Persist last searched city (localStorage) + * - [ ] Add error retry button component + * - [ ] Add favorites list (pin cities) * Advanced: - * - [ ] Hourly forecast visualization (line / area chart) - * - [x] Animate background transitions - * - [ ] Add geolocation: auto-detect user city (with permission) + * - [ ] Hourly forecast visualization (line / area chart) + * - [ ] Animate background transitions + * - [ ] Add geolocation: auto-detect user city (with permission) + * - [ ] Extract API call into /src/services/weather.js and add caching */ import { useEffect, useState } from "react"; @@ -31,133 +31,244 @@ import { getCacheStats, } from "../services/weather.js"; -// Helper to determine weather background class -const weatherToClass = (desc = "") => { - if (!desc) return "weather-bg-default"; - desc = desc.toLowerCase(); - if (desc.includes("rain") || desc.includes("shower") || desc.includes("drizzle")) - return "weather-bg-rain"; - if (desc.includes("snow") || desc.includes("blizzard")) - return "weather-bg-snow"; - if (desc.includes("cloud") || desc.includes("overcast")) - return "weather-bg-cloud"; - if (desc.includes("sun") || desc.includes("clear") || desc.includes("fair")) - return "weather-bg-sunny"; - if (desc.includes("fog") || desc.includes("mist") || desc.includes("haze") || desc.includes("smoke")) - return "weather-bg-fog"; - if (desc.includes("thunder") || desc.includes("storm")) - return "weather-bg-storm"; - return "weather-bg-default"; -}; - -// Render decorative weather animations -function renderWeatherAnimation(variant) { - if (variant === "sunny") { - return ( -
-
-
- ); - } +export default function Weather() { + const [city, setCity] = useState(""); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [unit, setUnit] = useState("C"); // °C by default + const [activeBg, setActiveBg] = useState("default"); + const [prevBg, setPrevBg] = useState(null); + const [isLocAllowed, setIsLocAllowed] = useState(null); + const [isRequestingLoc, setIsRequestingLoc] = useState(false); - if (variant === "cloud") { - return ( - <> - - - - - - - - - - - - ); - } + useEffect(() => { + const storedCity = localStorage.getItem("userLocation"); + if (storedCity) { + setIsLocAllowed(true); + setCity(JSON.parse(storedCity)); + } else if (navigator.geolocation) { + requestLocation(); + } else { + setIsLocAllowed(false); + setError( + "Your browser does not support location detection. Please enter city manually." + ); + setCity("London"); + } + }, []); - if (variant === "rain") { - return ( -
- {Array.from({ length: 12 }).map((_, i) => ( - - ))} -
- ); - } + useEffect(() => { + if (city) { + fetchWeather(city); + } + }, [city]); - if (variant === "snow") { - return ( -
- {Array.from({ length: 12 }).map((_, i) => ( - - ))} -
- ); + async function getCurrentCity(lat, lon) { + const APIkey = import.meta.env.VITE_WEATHER_API_KEY; + try { + const res = await fetch( + `https://api.openweathermap.org/geo/1.0/reverse?lat=${lat}&lon=${lon}&appid=${APIkey}` + ); + const data = await res.json(); + if (data && data.length > 0 && data[0].name) { + setCity(data[0].name); + setError(null); + setIsLocAllowed(true); + localStorage.setItem("userLocation", JSON.stringify(data[0].name)); + } else { + setCity("London"); + setError("Could not detect city from location."); + setIsLocAllowed(false); + } + } catch (err) { + console.log(err); + setCity("London"); + setError(err.message); + setIsLocAllowed(false); + } } - if (variant === "fog") { - return ( - <> -
-
- - ); - } + function requestLocation() { + setIsRequestingLoc(true); + navigator.geolocation.getCurrentPosition( + async function onSuccess(position) { + await getCurrentCity( + position.coords.latitude, + position.coords.longitude + ); + setIsRequestingLoc(false); + }, - if (variant === "storm") { - return ( -
-
-
-
+ function onError(err) { + console.log("Error", err); + setIsLocAllowed(false); + setError( + "Location is blocked. Please enable location in your browser settings to detect automatically." + ); + setCity("London"); + setIsRequestingLoc(false); + } ); } - return null; -} + // Helper to determine weather background class + const weatherToClass = (desc = "") => { + if (!desc) return "weather-bg-default"; + desc = desc.toLowerCase(); + if ( + desc.includes("rain") || + desc.includes("shower") || + desc.includes("drizzle") + ) + return "weather-bg-rain"; + if (desc.includes("snow") || desc.includes("blizzard")) + return "weather-bg-snow"; + if (desc.includes("cloud") || desc.includes("overcast")) + return "weather-bg-cloud"; + if (desc.includes("sun") || desc.includes("clear") || desc.includes("fair")) + return "weather-bg-sunny"; + if ( + desc.includes("fog") || + desc.includes("mist") || + desc.includes("haze") || + desc.includes("smoke") + ) + return "weather-bg-fog"; + if (desc.includes("thunder") || desc.includes("storm")) + return "weather-bg-storm"; + return "weather-bg-default"; + }; -export default function Weather() { - const [city, setCity] = useState(() => localStorage.getItem("lastCity") || "London"); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [unit, setUnit] = useState("C"); - const [activeBg, setActiveBg] = useState("default"); - const [prevBg, setPrevBg] = useState(null); + // Render decorative weather animations + function renderWeatherAnimation(variant) { + if (variant === "sunny") { + return ( +
+
+
+ ); + } - useEffect(() => { - fetchWeather(city); - }, []); + if (variant === "cloud") { + return ( + <> + + + + + + + + + + + + ); + } + + if (variant === "rain") { + return ( +
+ {Array.from({ length: 12 }).map((_, i) => ( + + ))} +
+ ); + } + + if (variant === "snow") { + return ( +
+ {Array.from({ length: 12 }).map((_, i) => ( + + ))} +
+ ); + } + + if (variant === "fog") { + return ( + <> +
+
+ + ); + } + + if (variant === "storm") { + return ( +
+
+
+
+ ); + } + + return null; + } async function fetchWeather(c) { try { setLoading(true); setError(null); - const json = await getWeatherData(c); + + // Try using optional service helper first (if exported) + let json = null; + if (typeof getWeatherData === "function") { + try { + json = await getWeatherData(c); + } catch (e) { + // if service fails, we'll fallback to wttr.in below + json = null; + } + } + + // Fallback to wttr.in if no json from service + if (!json) { + const res = await fetch( + `https://wttr.in/${encodeURIComponent(c)}?format=j1` + ); + if (!res.ok) throw new Error("Failed to fetch"); + json = await res.json(); + } + setData(json); localStorage.setItem("lastCity", c); } catch (e) { - setError(e); + setError(e?.message || String(e)); + setData(null); } finally { setLoading(false); } @@ -227,7 +338,10 @@ export default function Weather() { {renderWeatherAnimation(variant)}
-
+

🌤️ Weather Dashboard

@@ -236,7 +350,22 @@ export default function Weather() { onChange={(e) => setCity(e.target.value)} placeholder="Enter city" /> - + +
@@ -305,7 +434,11 @@ export default function Weather() { day.hourly?.[0]?.weatherDesc?.[0]?.value || "forecast icon" } - style={{ width: 40, height: 40, objectFit: "contain" }} + style={{ + width: 40, + height: 40, + objectFit: "contain", + }} onError={(e) => (e.currentTarget.style.display = "none") }