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
45,656 changes: 20,224 additions & 25,432 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@hookform/resolvers": "^1.3.5",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"axios": "^1.4.0",
"bootstrap": "^5.2.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.1"
"react-hook-form": "^6.15.4",
"react-redux": "^8.0.5",
"react-scripts": "^5.0.1",
"react-sparklines": "^1.7.0",
"redux": "^4.2.1",
"redux-form": "^8.3.10",
"redux-promise": "^0.6.0",
"web-vitals": "^1.1.1",
"yup": "^1.2.0"
},
"scripts": {
"start": "react-scripts start",
Expand Down
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Weather Redux</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
18 changes: 18 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import axios from "axios";

export const FETCH_FORECAST = "FETCH_FORECAST";

// stored API key in environment variable
const API_KEY = process.env.REACT_APP_WEATHER_API_KEY;
const urlForecast = 'https://api.openweathermap.org/data/2.5/forecast?q='

// action to get forecast data from weather API
export function fetchForecast(query) {
const request = axios.get(`${urlForecast}${query.city}&appid=${API_KEY}&units=imperial`)

return {
type: FETCH_FORECAST,
payload: request
}
}

69 changes: 69 additions & 0 deletions src/components/forecast-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Sparklines, SparklinesLine, SparklinesReferenceLine } from 'react-sparklines';
import { useSelector } from 'react-redux';
import _ from 'lodash';


const ForecastList = () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I see a lot of inconsistency in how you are writing functions (expression/declaration). It is good to stay consistent.

// pulled state in to be able to render all forecast data
const forecasts = useSelector((state) => state);

// function to render forecast data using Sparklines to show the 5 day forecast of temperature, pressure, and humidity. Includes reference line to show 5 day average.
const renderForecasts = () => {
if (!_.isEmpty(forecasts.entries)) {
return forecasts.cityIds.map((id) => {
return (
<tr key={id}>
<td>{forecasts.entries[id].city}</td>
<td>
<Sparklines data={forecasts.entries[id].temps} height={150}>
<SparklinesLine color='orange'/>
<SparklinesReferenceLine type='mean'/>
</Sparklines>

{/* function to display the 5 day average value. Is there an easier way to do this? Since ReferenceLine is also calculating average, can a value be pulled from that? */}
<div>{Math.round(forecasts.entries[id].temps.reduce((a, b) => a + b) / forecasts.entries[id].temps.length)} &deg;F</div>
</td>
<td>
<Sparklines data={forecasts.entries[id].pressure} height={150}>
<SparklinesLine color='green'/>
<SparklinesReferenceLine type='mean'/>
</Sparklines>
<div>{Math.round(forecasts.entries[id].pressure.reduce((a, b) => a + b) / forecasts.entries[id].pressure.length)} hPa</div>
</td>
<td>
<Sparklines data={forecasts.entries[id].humidity} height={150}>
<SparklinesLine color='blue'/>
<SparklinesReferenceLine type='mean'/>
</Sparklines>
<div>{Math.round(forecasts.entries[id].humidity.reduce((a, b) => a + b) / forecasts.entries[id].humidity.length)} %</div>
</td>
</tr>
)
});
} else {
return <tr><td>*Enter a city above for a forecast*</td></tr>
}
}

return (
<div>
<table className='table text-center align-middle'>
<thead>
<tr>
<th scope='col' className='col-md-3'>City</th>
<th scope='col' className='col-md-3'>Temperature(&deg;F)</th>
<th scope='col' className='col-md-3'>Pressure(hPa)</th>
<th scope='col' className='col-md-3'>Humidity(%)</th>
</tr>
</thead>

<tbody>
{renderForecasts()}
</tbody>
</table>
</div>

)
}

export default ForecastList;
16 changes: 16 additions & 0 deletions src/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const header = (props) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I really like how you broke this out into components, nice work!

return (
<div>
<div className='jumbotron text-center'>
<div className="container">
<h1 className="jumbotron-heading">Redux Weather</h1>
</div>
</div>
<div className='container'>
{props.children}
</div>
</div>
)
};

export default header;
48 changes: 48 additions & 0 deletions src/components/search-bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useForm } from "react-hook-form";
import * as Yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useDispatch } from "react-redux";
import { fetchForecast } from "../actions";

// not sure if necessary, but using yup to add form validation
const postSchema = Yup.object().shape({
city: Yup.string().required()
});

const SearchBar = () => {

// using react-hook-form for form validation
const { register, handleSubmit, errors, reset } = useForm({
resolver: yupResolver(postSchema)
});

const dispatch = useDispatch();

//dispatches action when form is submitted
const handleFormSubmit = (query) => {
dispatch(
fetchForecast(query)
)

// clears value from input once submitted
reset();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing some semi-colons


return (
<form onSubmit={handleSubmit(handleFormSubmit)} className='container row'>
<div className='form-group'>
<label className='form-label'>Get a 5-day forecast</label>
</div>
<div className='mb-3'>
<div className='input-group'>
<input className='form-control' type='text' placeholder='Please enter a city and click Search' name='city' ref={register}></input>
<button className='btn btn-primary' type='submit'>Search</button>
</div>
{errors.city?.message}
</div>
<hr />
</form>
)
}

export default SearchBar;
17 changes: 7 additions & 10 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
.jumbotron {
padding: 30px 20px;
margin-bottom: 2rem;
background-color: #e9ecef;
border-radius: 0.3rem;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
.forecast-data {
line-height: 200px;
}
29 changes: 19 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import 'bootstrap/dist/css/bootstrap.css';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { createStore, applyMiddleware } from 'redux';
import promise from 'redux-promise';
import { Provider } from 'react-redux';

import Header from './components/header';
import SearchBar from './components/search-bar';
import ForecastList from './components/forecast-list';
import forecastReducer from './reducers/reducer-forecast';

const createStoreWithMiddleware = applyMiddleware(promise)(createStore);

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
<Provider store={createStoreWithMiddleware(forecastReducer)}>
<React.StrictMode>
<Header>
<SearchBar />
<ForecastList />
</Header>
</React.StrictMode>
</Provider>,
document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
30 changes: 30 additions & 0 deletions src/reducers/reducer-forecast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FETCH_FORECAST } from "../actions";
import _ from 'lodash';

const DEFAULT_STATE = {
entries: {},
cityIds: []
};

// the entries parameter in state will be an object of objects that lists the cities by their id and contains data for city name, temp, pressure, and humidity. cityIds was added as an array so the id's could be mapped through and used to select the correct objects within entries when rendering forecasts in ./components/forecast-list.js
const forecastReducer = function (state = DEFAULT_STATE, action) {
switch (action.type) {
case FETCH_FORECAST:
return {
entries: { ...state.entries, [action.payload.data.city.id]: {
city: action.payload.data.city.name,

// creates an array of all the 5 day forecast data for temp, pressure, and humidity. The array will be used for sparklines
temps: action.payload.data.list.map(obj => obj.main.temp),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Don't recommend using generic terms like obj to name things. Be as descriptive as possible with naming.

pressure: action.payload.data.list.map(obj => obj.main.pressure),
humidity: action.payload.data.list.map(obj => obj.main.humidity)
}
},
cityIds: _.union([...state.cityIds], [action.payload.data.city.id])
}
default:
return state;
}
}

export default forecastReducer;