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
@@ -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")
}