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
44,524 changes: 20,067 additions & 24,457 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^1.9.7",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"axios": "^1.6.1",
"bootstrap": "^3.4.1",
"lodash": "^4.17.21",
"normalizr": "^3.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"react-redux": "^8.1.3",
"react-router-dom": "^6.18.0",
"react-scripts": "^5.0.1",
"react-sparklines": "^1.7.0",
"redux": "^4.2.1",
"redux-promise": "^0.6.0",
"web-vitals": "^1.1.1"
},
"scripts": {
Expand All @@ -19,6 +29,7 @@
},
"eslintConfig": {
"extends": [
"wesbos",
"react-app",
"react-app/jest"
]
Expand All @@ -34,5 +45,12 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/lodash": "^4.14.201",
"eslint": "^8.0.0",
"eslint-config-wesbos": "^3.2.3",
"prettier": "^2.7.1",
"typescript": "^4.8.4"
}
}
38 changes: 0 additions & 38 deletions src/App.css

This file was deleted.

25 changes: 0 additions & 25 deletions src/App.js

This file was deleted.

8 changes: 0 additions & 8 deletions src/App.test.js

This file was deleted.

132 changes: 132 additions & 0 deletions src/components/forecasts-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'bootstrap/dist/css/bootstrap.min.css';

// eslint-disable-next-line import/no-extraneous-dependencies
import { useSelector, useStore } from 'react-redux';
import {
Sparklines,
SparklinesLine,
SparklinesReferenceLine,
} from 'react-sparklines';
import { useState } from 'react';
import { fetchFiveDayForecast } from '../reducers/forecastSlice';

/**
* Forecast DOM component
* @returns {DOM element}
*/
const ForecastsIndex = () => {
// store variable declaration and variable to track state of store
const store = useStore();
const forecasts = useSelector((state) => state);
// mutable variables to hold forecast data properties
let searchedCity = 'City Name';
let averageTemp = 0;
let averagePressure = 0;
let averageHumidity = 0;
let tempsArray = [];
let pressureArray = [];
let humidityArray = [];

// checks that returned api call data array is not empty then assigns variable values
if (forecasts.forecasts.forecasts.length !== 0) {
searchedCity = forecasts.forecasts.forecasts[0].city.name;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Would recommend not to hard code an index like this, use array method to find the value you need instead

tempsArray = forecasts.forecasts.forecasts[0].temp;
pressureArray = forecasts.forecasts.forecasts[0].pressure;
humidityArray = forecasts.forecasts.forecasts[0].humidity;
averageTemp = calculateAverage(tempsArray);
averageHumidity = calculateAverage(humidityArray);
averagePressure = calculateAverage(pressureArray);
}
// variable for user input search and input string to component state
const [searchInput, setSearch] = useState('');
// variable to check that searchInput variable exists - used to enable/disable submit button for appropriate condition of weather forecast search
const searchFormValid = Boolean(searchInput);

/**
*
* calculates average value of array
* @param {array}
* @returns {average}
*
*/
function calculateAverage(array) {
let average = 0;
if (array.length !== 0) {
const sum = array.reduce((acc, value) =>
Math.floor(Math.round(acc + value))
);
average = Math.round(sum / array.length);
}
return average;
}

const handleSearchOnClick = () => {
store.dispatch(fetchFiveDayForecast(searchInput));
};

/**
* Function to render a Sparklines visual graph of input array plot points
* with median line for average of array
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nice job on the documentation

* @param {Array} valuesArray
* @returns {Sparklines element}
*/
function renderForecastValues(valuesArray) {
return (
<Sparklines data={valuesArray}>
<SparklinesLine />
<SparklinesReferenceLine type="mean" />
</Sparklines>
);
}

return (
<div className="forecasts-index">
<header className="header">
<div className="cityName">
<h1>
<span style={{ fontWeight: 'bold' }}>{searchedCity}</span> -{' '}
<span style={{ fontStyle: 'italic' }}>Five Day Forecast:</span>
</h1>
</div>
<div className="forecast-displays">
<div id="temperature" className="col-md-4">
<h2>Temperatures (Average - {averageTemp}° F):</h2>
<div>{renderForecastValues(tempsArray)}</div>
<div id="temperature-chart" />
</div>
<div id="pressure" className="col-md-4">
<h2>Pressure (Average - {averagePressure} hPa):</h2>
<div>{renderForecastValues(pressureArray)}</div>
<div id="pressure-chart" />
</div>
<div id="humidity" className="col-md-4">
<h2>Humidity (Average - {averageHumidity}%):</h2>
<div>{renderForecastValues(humidityArray)}</div>
<div id="humidity-chart" />
</div>
</div>
<form className="search-form">
<div className="form-group col-md-4 m-2">
<input
type="text"
id="search-query"
className="form-control col-md-1"
placeholder="City"
onChange={(event) => setSearch(event.target.value)}
/>
<button
type="button"
className="btn btn-primary search"
onClick={handleSearchOnClick}
disabled={!searchFormValid}
>
Search
</button>
</div>
</form>
</header>
</div>
);
};

export default ForecastsIndex;
13 changes: 13 additions & 0 deletions src/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// header DOM component
const header = (props) => (
<div>
<div className="jumbotron text-center">
<div className="container">
<h1 className="jumbotron-heading">Weather Forecast</h1>
</div>
</div>
<div className="container">{props.children}</div>
</div>
);

export default header;
20 changes: 16 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import 'bootstrap/dist/css/bootstrap.css';
import React from 'react';
import ReactDOM from 'react-dom';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Provider } from 'react-redux';
// eslint-disable-next-line import/no-extraneous-dependencies
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { store } from './store';
import './index.css';
import App from './App';
import ForecastsIndex from './components/forecasts-index';
import Header from './components/header';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
<Provider store={store}>
<BrowserRouter>
<Header />
<Routes>
<Route path="/" Component={ForecastsIndex} />
</Routes>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);

Expand Down
76 changes: 76 additions & 0 deletions src/reducers/forecastSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

// proprietary api key for api calls
const apiKey = 'e762b32d14efd802e7f067526402633f';

// initialState variable
const initialState = {
forecasts: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};

/**
* @param city
* @param apiKey
* @param thunkApi
* thunk variable to make api call with @city and @apiKey inputs
* returns a promise with data or error on failed call
*/
export const fetchFiveDayForecast = createAsyncThunk(
'reducers/fetchFiveDayForecast',
async (city, thunkApi) => {
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${city}&appid=${apiKey}&units=imperial`
);
return response;
} catch (err) {
if (!err?.response) {
throw err;
}
return thunkApi.rejectWithValue({ err: 'Error with forecast query' });
}
}
);

// slice variable with builder function to add cases for processing returned Promise from api call. Fulfilled promise changes state with returned data from Promise
const forecastSlice = createSlice({
name: 'forecasts',
initialState,
extraReducers: (builder) => {
builder
.addCase(fetchFiveDayForecast.pending, (state) => {
state.loading = true;
})
.addCase(fetchFiveDayForecast.fulfilled, (state, action) => {
const tempArray = action.payload.data.list.map(
(item) => item.main.temp
);
const pressureArray = action.payload.data.list.map(
(item) => item.main.pressure
);
const humidityArray = action.payload.data.list.map(
(item) => item.main.humidity
);
const searchReturnData = {
city: action.payload.data.city,
temp: tempArray,
pressure: pressureArray,
humidity: humidityArray,
};
state.forecasts = [searchReturnData];
state.succeeded = true;
state.error = undefined;
})
.addCase(fetchFiveDayForecast.rejected, (state, action) => {
state.data = undefined;
state.failed = true;
state.error = action?.payload;
});
},
});

export default forecastSlice.reducer;
2 changes: 1 addition & 1 deletion src/reportWebVitals.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const reportWebVitals = onPerfEntry => {
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
Expand Down
17 changes: 17 additions & 0 deletions src/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
// eslint-disable-next-line import/no-extraneous-dependencies
import forecastSlice from './reducers/forecastSlice';

// default middleware with serializableCheck set to false
const nonSerializedMiddleware = getDefaultMiddleware({
serializableCheck: false,
});

// configured Store variable to handle state of store for App
export const store = configureStore({
reducer: { forecasts: forecastSlice },
initialState: [
{ city: '', forecastData: { temp: [], pressure: [], humidity: [] } },
],
middleware: nonSerializedMiddleware,
});