From 576547df19b8ca78f4f4dad69964525416b3418c Mon Sep 17 00:00:00 2001 From: Priya Nandwani Date: Sun, 19 Oct 2025 17:59:18 +0530 Subject: [PATCH] Add favourites feature to Weather Dashboard --- package.json | 1 + src/components/SprinkleEffect.jsx | 44 ++++++++ src/pages/Weather.jsx | 166 +++++++++++++++++++++++++----- src/styles.css | 108 +++++++++++++++++++ 4 files changed, 291 insertions(+), 28 deletions(-) create mode 100644 src/components/SprinkleEffect.jsx diff --git a/package.json b/package.json index 8d15f9e..dca5e69 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "lucide-react": "^0.546.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.5.0", "react-leaflet": "^4.2.1", "react-router-dom": "^6.27.0" }, diff --git a/src/components/SprinkleEffect.jsx b/src/components/SprinkleEffect.jsx new file mode 100644 index 0000000..df931d0 --- /dev/null +++ b/src/components/SprinkleEffect.jsx @@ -0,0 +1,44 @@ +import React, { useEffect } from "react"; + +const SprinkleEffect = ({ trigger }) => { + useEffect(() => { + if (!trigger) return; // do nothing if trigger is null + + const { x, y } = trigger; + + // Create multiple spark dots + for (let i = 0; i < 10; i++) { + const dot = document.createElement("span"); + dot.classList.add("spark"); + + // Fixed positioning so it appears at correct spot + dot.style.position = "fixed"; + dot.style.left = x + "px"; + dot.style.top = y + "px"; + dot.style.pointerEvents = "none"; // prevent interference with clicks + document.body.appendChild(dot); + + const angle = Math.random() * 2 * Math.PI; + const distance = Math.random() * 30 + 10; + const dx = Math.cos(angle) * distance; + const dy = Math.sin(angle) * distance; + + dot.animate( + [ + { transform: "translate(0,0)", opacity: 1 }, + { transform: `translate(${dx}px, ${dy}px)`, opacity: 0 }, + ], + { duration: 700, easing: "ease-out" } + ); + + // Remove dot after animation + setTimeout(() => dot.remove(), 700); + } + }, [trigger]); + + return null; // nothing to render +}; + +export default SprinkleEffect; + + diff --git a/src/pages/Weather.jsx b/src/pages/Weather.jsx index 7c46851..969fa88 100644 --- a/src/pages/Weather.jsx +++ b/src/pages/Weather.jsx @@ -21,18 +21,21 @@ * - [ ] Extract API call into /src/services/weather.js and add caching */ -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import Loading from "../components/Loading.jsx"; import ErrorMessage from "../components/ErrorMessage.jsx"; import Card from "../components/Card.jsx"; import Skeleton from "../components/Skeleton.jsx"; -import HeroSection from '../components/HeroSection'; -import Cloud from '../Images/Cloud.jpg'; +import HeroSection from "../components/HeroSection"; +import Cloud from "../Images/Cloud.jpg"; import { getWeatherData, clearWeatherCache, getCacheStats, } from "../services/weather.js"; +import { IoMdHeartEmpty } from "react-icons/io"; +import { IoMdHeart } from "react-icons/io"; +import SprinkleEffect from "../components/SprinkleEffect.jsx"; export default function Weather() { const [city, setCity] = useState(""); @@ -44,6 +47,10 @@ export default function Weather() { const [prevBg, setPrevBg] = useState(null); const [isLocAllowed, setIsLocAllowed] = useState(null); const [isRequestingLoc, setIsRequestingLoc] = useState(false); + const [trigger, setTrigger] = useState(null); + const [favourites, setFavourites] = useState([]); + const [showFavourites, setShowFavourites] = useState(false); + const btnRef = useRef(null); useEffect(() => { const storedCity = localStorage.getItem("userLocation"); @@ -156,15 +163,15 @@ export default function Weather() { if (variant === "cloud") { return ( <> - - Weather Wonders - - } - subtitle="Stay ahead of the weather with real-time updates and accurate forecasts tailored just for you" -/> + + Weather Wonders + + } + subtitle="Stay ahead of the weather with real-time updates and accurate forecasts tailored just for you" + /> { + setFavourites(JSON.parse(localStorage.getItem("favourites")) || []); + }, []); + + // handle add to favourite + const handleAddToFav = () => { + const rect = btnRef.current.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + + // Set trigger + setTrigger({ x, y }); + + // Reset trigger after a short delay to prevent unwanted reruns + setTimeout(() => setTrigger(null), 50); + + let updatedFav; + const formattedCity = formatCityName(city); + const isFav = favourites.some( + (c) => c.toLowerCase() === city.toLowerCase() + ); + updatedFav = isFav + ? favourites.filter((item) => item.toLowerCase() !== city.toLowerCase()) + : [...favourites, formattedCity]; + + setFavourites(updatedFav); + localStorage.setItem("favourites", JSON.stringify(updatedFav)); + }; + + const isFav = favourites.some((c) => c.toLowerCase() === city.toLowerCase()); + + function formatCityName(str) { + return str + .split(" ") + .map((word) => word[0].toUpperCase() + word.slice(1)) + .join(" "); + } + return (
-
+
@@ -403,18 +448,70 @@ export default function Weather() { > Switch to °{unit === "C" ? "F" : "C"} + + {showFavourites && ( +
+ +
+ )}
{loading && } {error && ( - fetchWeather(city)} /> + fetchWeather(city)} + /> )} {data && !loading && (
{/* Current Weather */} -

{data.nearest_area?.[0]?.areaName?.[0]?.value || city}

+
+

{data.nearest_area?.[0]?.areaName?.[0]?.value || city}

+
+ {isFav ? ( + + ) : ( + + )} +
+ +
+

{current && getIconUrl(current.weatherIconUrl) && ( { - const condition = day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; + const condition = + day.hourly?.[0]?.weatherDesc?.[0]?.value || "Clear"; const badge = getBadgeStyle(condition); return ( - {day.hourly?.[0] && getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( -

- {day.hourly?.[0]?.weatherDesc?.[0]?.value (e.currentTarget.style.display = "none")} - /> -
- )} - -
+ {day.hourly?.[0] && + getIconUrl(day.hourly?.[0]?.weatherIconUrl) && ( +
+ { + (e.currentTarget.style.display = "none") + } + /> +
+ )} + +
Avg Temp:{" "} {displayTemp(Number(day.avgtempC))}°{unit}