2121 * - [ ] Extract API call into /src/services/weather.js and add caching
2222 */
2323
24- import { useEffect , useState } from "react" ;
24+ import { useEffect , useState , useRef } from "react" ;
2525import Loading from "../components/Loading.jsx" ;
2626import ErrorMessage from "../components/ErrorMessage.jsx" ;
2727import Card from "../components/Card.jsx" ;
2828import Skeleton from "../components/Skeleton.jsx" ;
29- import HeroSection from ' ../components/HeroSection' ;
30- import Cloud from ' ../Images/Cloud.jpg' ;
29+ import HeroSection from " ../components/HeroSection" ;
30+ import Cloud from " ../Images/Cloud.jpg" ;
3131import {
3232 getWeatherData ,
3333 clearWeatherCache ,
3434 getCacheStats ,
3535} from "../services/weather.js" ;
36+ import { IoMdHeartEmpty } from "react-icons/io" ;
37+ import { IoMdHeart } from "react-icons/io" ;
38+ import SprinkleEffect from "../components/SprinkleEffect.jsx" ;
3639
3740export default function Weather ( ) {
3841 const [ city , setCity ] = useState ( "" ) ;
@@ -44,6 +47,10 @@ export default function Weather() {
4447 const [ prevBg , setPrevBg ] = useState ( null ) ;
4548 const [ isLocAllowed , setIsLocAllowed ] = useState ( null ) ;
4649 const [ isRequestingLoc , setIsRequestingLoc ] = useState ( false ) ;
50+ const [ trigger , setTrigger ] = useState ( null ) ;
51+ const [ favourites , setFavourites ] = useState ( [ ] ) ;
52+ const [ showFavourites , setShowFavourites ] = useState ( false ) ;
53+ const btnRef = useRef ( null ) ;
4754
4855 useEffect ( ( ) => {
4956 const storedCity = localStorage . getItem ( "userLocation" ) ;
@@ -156,15 +163,15 @@ export default function Weather() {
156163 if ( variant === "cloud" ) {
157164 return (
158165 < >
159- < HeroSection
160- image = { Cloud }
161- title = {
162- < >
163- Weather < span style = { { color : "black" } } > Wonders</ span >
164- </ >
165- }
166- subtitle = "Stay ahead of the weather with real-time updates and accurate forecasts tailored just for you"
167- />
166+ < HeroSection
167+ image = { Cloud }
168+ title = {
169+ < >
170+ Weather < span style = { { color : "black" } } > Wonders</ span >
171+ </ >
172+ }
173+ subtitle = "Stay ahead of the weather with real-time updates and accurate forecasts tailored just for you"
174+ />
168175 < svg
169176 className = "cloud-svg cloud--left"
170177 viewBox = "0 0 220 80"
@@ -345,6 +352,44 @@ export default function Weather() {
345352 return { color : "#E0E0E0" , label : "Clear 🌤️" } ;
346353 } ;
347354
355+ useEffect ( ( ) => {
356+ setFavourites ( JSON . parse ( localStorage . getItem ( "favourites" ) ) || [ ] ) ;
357+ } , [ ] ) ;
358+
359+ // handle add to favourite
360+ const handleAddToFav = ( ) => {
361+ const rect = btnRef . current . getBoundingClientRect ( ) ;
362+ const x = rect . left + rect . width / 2 ;
363+ const y = rect . top + rect . height / 2 ;
364+
365+ // Set trigger
366+ setTrigger ( { x, y } ) ;
367+
368+ // Reset trigger after a short delay to prevent unwanted reruns
369+ setTimeout ( ( ) => setTrigger ( null ) , 50 ) ;
370+
371+ let updatedFav ;
372+ const formattedCity = formatCityName ( city ) ;
373+ const isFav = favourites . some (
374+ ( c ) => c . toLowerCase ( ) === city . toLowerCase ( )
375+ ) ;
376+ updatedFav = isFav
377+ ? favourites . filter ( ( item ) => item . toLowerCase ( ) !== city . toLowerCase ( ) )
378+ : [ ...favourites , formattedCity ] ;
379+
380+ setFavourites ( updatedFav ) ;
381+ localStorage . setItem ( "favourites" , JSON . stringify ( updatedFav ) ) ;
382+ } ;
383+
384+ const isFav = favourites . some ( ( c ) => c . toLowerCase ( ) === city . toLowerCase ( ) ) ;
385+
386+ function formatCityName ( str ) {
387+ return str
388+ . split ( " " )
389+ . map ( ( word ) => word [ 0 ] . toUpperCase ( ) + word . slice ( 1 ) )
390+ . join ( " " ) ;
391+ }
392+
348393 return (
349394 < div
350395 className = "weather-page"
@@ -390,7 +435,7 @@ export default function Weather() {
390435 </ button >
391436 </ form >
392437
393- < div className = "dev-tools" >
438+ < div className = "dev-tools" style = { { position : "relative" } } >
394439 < button onClick = { handleClearCache } className = "dev-btn" >
395440 Clear Cache
396441 </ button >
@@ -403,18 +448,70 @@ export default function Weather() {
403448 >
404449 Switch to °{ unit === "C" ? "F" : "C" }
405450 </ button >
451+ < button
452+ className = "dev-btn"
453+ onClick = { ( ) => setShowFavourites ( ( prev ) => ! prev ) }
454+ >
455+ { showFavourites ? "Hide Favourites" : " See Favourites" }
456+ </ button >
457+ { showFavourites && (
458+ < div className = "favourites-dropdown" >
459+ < select
460+ value = ""
461+ onChange = { ( e ) => {
462+ fetchWeather ( e . target . value ) ;
463+ setCity ( e . target . value ) ;
464+ setShowFavourites ( false ) ;
465+ } }
466+ >
467+ < option value = "" disabled >
468+ Select a favourite
469+ </ option >
470+ { favourites . map ( ( fav , i ) => (
471+ < option
472+ className = "favourites-option"
473+ value = { formatCityName ( fav ) }
474+ key = { formatCityName ( fav ) }
475+ >
476+ { formatCityName ( fav ) }
477+ </ option >
478+ ) ) }
479+ </ select >
480+ </ div >
481+ ) }
406482 </ div >
407483
408484 { loading && < Loading /> }
409485 { error && (
410- < ErrorMessage message = { error . message } onRetry = { ( ) => fetchWeather ( city ) } />
486+ < ErrorMessage
487+ message = { error . message }
488+ onRetry = { ( ) => fetchWeather ( city ) }
489+ />
411490 ) }
412491
413492 { data && ! loading && (
414493 < div className = "dashboard-grid" >
415494 { /* Current Weather */ }
416495 < Card title = "Current Weather" size = "large" >
417- < h2 > { data . nearest_area ?. [ 0 ] ?. areaName ?. [ 0 ] ?. value || city } </ h2 >
496+ < div
497+ style = { { display : "flex" , gap : "1rem" , alignItems : "baseline" } }
498+ >
499+ < h2 > { data . nearest_area ?. [ 0 ] ?. areaName ?. [ 0 ] ?. value || city } </ h2 >
500+ < div
501+ className = "fav-icon"
502+ ref = { btnRef }
503+ onClick = { handleAddToFav }
504+ title = { isFav ? "Remove from favourites" : "Add to favourites" }
505+ >
506+ { isFav ? (
507+ < IoMdHeart size = { 18 } color = "#b22222" />
508+ ) : (
509+ < IoMdHeartEmpty size = { 18 } color = "#b22222" />
510+ ) }
511+ </ div >
512+ < SprinkleEffect trigger = { trigger } />
513+ </ div >
514+
418515 < p style = { { display : "flex" , alignItems : "center" , gap : "12px" } } >
419516 { current && getIconUrl ( current . weatherIconUrl ) && (
420517 < img
@@ -446,23 +543,36 @@ export default function Weather() {
446543
447544 { /* 3-Day Forecast */ }
448545 { forecast . map ( ( day , i ) => {
449- const condition = day . hourly ?. [ 0 ] ?. weatherDesc ?. [ 0 ] ?. value || "Clear" ;
546+ const condition =
547+ day . hourly ?. [ 0 ] ?. weatherDesc ?. [ 0 ] ?. value || "Clear" ;
450548 const badge = getBadgeStyle ( condition ) ;
451549
452550 return (
453551 < Card key = { i } title = { i === 0 ? "Today" : `Day ${ i + 1 } ` } >
454- { day . hourly ?. [ 0 ] && getIconUrl ( day . hourly ?. [ 0 ] ?. weatherIconUrl ) && (
455- < div style = { { marginTop : 8 } } >
456- < img
457- src = { getIconUrl ( day . hourly ?. [ 0 ] ?. weatherIconUrl ) }
458- alt = { day . hourly ?. [ 0 ] ?. weatherDesc ?. [ 0 ] ?. value || "forecast icon" }
459- style = { { width : 40 , height : 40 , objectFit : "contain" } }
460- onError = { ( e ) => ( e . currentTarget . style . display = "none" ) }
461- />
462- </ div >
463- ) }
464-
465- < div style = { { display : "flex" , gap : "8px" , marginTop : "17px" } } >
552+ { day . hourly ?. [ 0 ] &&
553+ getIconUrl ( day . hourly ?. [ 0 ] ?. weatherIconUrl ) && (
554+ < div style = { { marginTop : 8 } } >
555+ < img
556+ src = { getIconUrl ( day . hourly ?. [ 0 ] ?. weatherIconUrl ) }
557+ alt = {
558+ day . hourly ?. [ 0 ] ?. weatherDesc ?. [ 0 ] ?. value ||
559+ "forecast icon"
560+ }
561+ style = { {
562+ width : 40 ,
563+ height : 40 ,
564+ objectFit : "contain" ,
565+ } }
566+ onError = { ( e ) =>
567+ ( e . currentTarget . style . display = "none" )
568+ }
569+ />
570+ </ div >
571+ ) }
572+
573+ < div
574+ style = { { display : "flex" , gap : "8px" , marginTop : "17px" } }
575+ >
466576 < strong > Avg Temp:</ strong > { " " }
467577 { displayTemp ( Number ( day . avgtempC ) ) } °{ unit }
468578 < div
0 commit comments