Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"cSpell.words": [
"accum",
"APIKEY",
"hookform",
"openweathermap",
"Sparklines"
]
}
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,43 @@ This project has been created by a student at Parsity, an online software engine

If you have any questions about this project or the program in general, visit [parsity.io](https://parsity.io/) or email hello@parsity.io.

# What is it?

A React weather app that fetches weather data using the [open weather api](https://openweathermap.org/) and stores the data using redux.

## Features

- Search for the weather data of a city using a text input and receive back current weather, 5 day forecast, and 5 day forecast in graph form.
- Search for the weather of current location using a button
- Set a default location to local storage
- Responsive components

## Component structure

```
Layout
|
|AppNavBar.js
|
|page.js
|
|SearchBar.jsx
| |
| |SearchBarForm.jsx
|
|WeatherPanel.jsx
|
|Panel.jsx
| |
| |CurrentWeather.jsx
|
|Weather.jsx
|
|FiveDayWeather.jsx
| |
| |DayBox.jsx
|
|Graphs.jsx
|
|Graph.jsx
```
67 changes: 67 additions & 0 deletions app/components/AppNavbar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"use client"
import { useDispatch, useSelector } from "react-redux";
import { setCurrentLocation } from "../store/slices/locations";
import { getWeather } from "./WeatherAPI";

export default function AppNavbar() {
const dispatch = useDispatch();
const defaultLocation = useSelector(state => state.locations.defaultLocation);

const onSuccess = async (data) => {
try {
if (!data) return
const { latitude, longitude } = data.coords;
const weatherData = await getWeather("", {lat : latitude, lon: longitude})
dispatch(setCurrentLocation(weatherData));
} catch (e) {
console.log(e);
}
};

const onError = (error) => {
console.log(error)
};

const handleSetLocation = (navigator, onSuccess, onError) => {
const geolocation = navigator.geolocation;
if (!geolocation) return alert("Geolocation is not supported. Sorry!");

geolocation.getCurrentPosition(onSuccess, onError, {enableHighAccuracy:true});
};

return (
<nav
className="navbar navbar-expand-lg navbar-light bg-light navbar-brand-center mb-3"
>
<a className="navbar-brand ms-3" href="#">RTK Weather</a>
<ul className="navbar-nav mx-auto">
<li className="nav-item navbar-text" id="default-city-text">
Default location: {defaultLocation ? defaultLocation.name : "Not Set"}
</li>
</ul>
<a
className="nav-link me-3 btn btn-primary"
type="button"
id="cur-loc-btn"
href="#"
onClick={() => handleSetLocation(navigator, onSuccess, onError)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-geo-alt"
viewBox="0 0 16 16"
>
<path
d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A32 32 0 0 1 8 14.58a32 32 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10"
/>
<path
d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4m0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6"
/>
</svg>
</a>
</nav>
)
}
15 changes: 15 additions & 0 deletions app/components/BootstrapClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import { useEffect } from "react";

function BootstrapClient() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this, next time group them in a separate folder like utils or packages

useEffect(() => {
require('bootstrap/dist/js/bootstrap.bundle.min.js');
}, []);

return null;
}

export default BootstrapClient;


22 changes: 22 additions & 0 deletions app/components/CurrentWeather.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Image from "next/image";

export default function CurrentWeather({ currentWeatherDetails }) {

return (
<div className="d-flex justify-content-around text-center">
<div className="cur-weather-text d-flex flex-column justify-content-center">
<div className="weather-degree">{currentWeatherDetails.temp}°</div>
<div className="weather-city">{currentWeatherDetails.name}</div>
<div className="weather-condition">{currentWeatherDetails.weather}</div>
</div>
<Image
src={`https://openweathermap.org/img/wn/${currentWeatherDetails.iconCode}@2x.png`}
alt={`${currentWeatherDetails.weather} icon`}
width={100}
height={100}
priority
className="cur-weather-icon"
/>
</div>
)
}
34 changes: 34 additions & 0 deletions app/components/DayBox.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Image from "next/image";
import {v4 as uuidv4} from "uuid";
import { useEffect, useState } from "react";

export default function DayBox({ day }) {
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);


return (
<div key={uuidv4()} className={`five-day-box col d-flex flex-column align-items-center justify-content-between ${width <= 763 && "border-bottom"} `}>
<div className="weather-condition-sm">{day.weather}</div>
<div className="weather-degree-sm">{day.temp}°</div>
<Image
src={`https://openweathermap.org/img/wn/${day.iconCode}@2x.png`}
alt={`${day.weather} icon`}
className="cur-weather-icon"
width={50} height={50}
/>
<div className="weather-day">{day.day}</div>
</div>
);
}
8 changes: 8 additions & 0 deletions app/components/ErrorMessage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export default function Error({message }) {
return (
<div className="text-danger">
<p>Error: {message}</p>
</div>
)
}
13 changes: 13 additions & 0 deletions app/components/FiveDayWeather.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {v4 as uuidv4} from "uuid";
import DayBox from "./DayBox";

export default function FiveDayWeather({ fiveDayWeather }) {

return (
<div className="five-day-panel row flex-column flex-md-row text-center">
{ fiveDayWeather.map(day =>
<DayBox key={uuidv4()} day={day} />)
}
</div>
);
}
19 changes: 19 additions & 0 deletions app/components/Graph.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { v4 as uuidv4 } from "uuid";
import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-sparklines";

export default function Graph({ data, name }) {
// calculate the rounded average of the values array
const average = data.reduce((sum, cur) => sum + cur, 0) / data.length;
const roundedAverage = Math.ceil(average * 100) / 100;

return (
<div className="col-xs-12 col-sm-4 d-flex flex-column" key={uuidv4()}>
{name.toUpperCase()}
<Sparklines data={data}>
<SparklinesLine color="blue" />
<SparklinesReferenceLine type="avg" />
</Sparklines>
<span> Avg: {roundedAverage} </span>
</div>
)
}
20 changes: 20 additions & 0 deletions app/components/Graphs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { v4 as uuidv4 } from "uuid";
import Graph from "./Graph";

export default function Graphs ({ fiveDayWeather }) {
// Reduce five day weather to an object {temp: [], humidity:[], pressure:[]}.
const graphData = fiveDayWeather.reduce((accum, day) => {
accum.temp ? accum.temp.push(day.temp) : accum.temp = [day.temp];
accum.humidity ? accum.humidity.push(day.humidity) : accum.humidity = [day.humidity];
accum.pressure ? accum.pressure.push(day.pressure) : accum.pressure = [day.pressure];
return accum;
}, {});

return (
<div className="five-day-panel row text-center">
{Object.keys(graphData).map(key =>
<Graph key={uuidv4()} data={graphData[key]} name={key} />
)}
</div>
)
}
49 changes: 49 additions & 0 deletions app/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useState } from "react";

export default function Modal ({action, text, title = "Modal", body= "", closeText = "Close", submitText="Save Changes"}) {
const [show, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = (e) => {
e.preventDefault()
setShow(true)
};

const handleSubmit = (action) => {
console.log("Handling set default")
action();
handleClose();
}

return (
<>
<a className="no-decorations" href="" onClick={(e) => {handleShow(e)}}>
{text}
</a>

{/* Modal */}
<div className={`modal fade ${show ? 'show d-block' : ''}`} tabIndex="-1" role="dialog">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">{title}</h5>
</div>
<div className="modal-body">
<p>{body}</p>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={handleClose}>
{closeText}
</button>
<button type="button" className="btn btn-primary" onClick={() => {handleSubmit(action)}}>
{submitText}
</button>
</div>
</div>
</div>
</div>

{/* Background overlay for modal */}
{show && <div className="modal-backdrop fade show"></div>}
</>
);
};
12 changes: 12 additions & 0 deletions app/components/Panel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Weather from "./Weather";
import CurrentWeather from "./CurrentWeather";

export default function Panel({ weatherDetails}) {

return (
<div className="weather-panel row d-flex justify-content-center w-75 border">
<CurrentWeather currentWeatherDetails={weatherDetails.currentWeather} />
<Weather weatherDetails={weatherDetails} />
</div>
)
}
48 changes: 48 additions & 0 deletions app/components/SearchBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client"
import { useState } from "react";
import { getWeather } from "./WeatherAPI";
import { useDispatch } from "react-redux";
import { pushLocation } from "../store/slices/locations";
import SearchBarForm from "./SearchBarForm";
import useReturnDataValidation from "./useReturnDataValidation"

export default function SearchBar() {
const [searchErrors, setSearchErrors] = useState(null);
const dispatch = useDispatch();

const weatherSchema = useReturnDataValidation()

const handleFormSubmit = async (inputText) => {
// Reset errors
setSearchErrors(null);

try {
// Get location based on city name
const weather = await getWeather(inputText);

// Validate Api data using yup
await weatherSchema.validate(weather);

// update redux
dispatch(pushLocation(weather));

} catch (e) {
// On validation fail set error message
setSearchErrors({message: `Could not find the city ${inputText}.`});
}
};


return (
<section className="container">
<div className="row justify-content-center">
<div className="col col-9">
<SearchBarForm
handleFormSubmit={handleFormSubmit}
returnDataError={searchErrors}
/>
</div>
</div>
</section>
)
}
Loading