-
Notifications
You must be signed in to change notification settings - Fork 36
RTK Weather - Mark Smyth #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
960e341
0152258
8a8cb78
a23113d
dd972d0
d2c0b90
166eeb5
43b2c07
031d2c0
4a9427b
a9f9880
3938064
2d42d3e
463fffd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,17 @@ | ||
| ## Weather Project | ||
|
|
||
| This project has been created by a student at Parsity, an online software engineering course. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks. | ||
| This project has been created by Mark Smyth, a student at Parsity, an online software engineering course. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks. | ||
|
|
||
| This application allows the user to enter a city and it returns a 5 day forecast in the form of sparkline charts. The user can add multiple cities which will create a list of weather forecasts. Individual city forecasts can be deleted by clicking the "Delete" button. | ||
|
|
||
| If you have any questions about this project or the program in general, visit [parsity.io](https://parsity.io/) or email hello@parsity.io. | ||
|
|
||
| Instructions to Run Application | ||
|
|
||
| 1. Open terminal | ||
| 2. Open directory that includes project | ||
| 3. In terminal, type : "npm install" | ||
| 4. Once npm is installed, type: "npm run dev" | ||
| 5. In browser, go to URL provided (usually: http://localhost:3000) | ||
|
|
||
| NOTE: To run this program, a valid API key for OpenWeather is needed. If needed, I will send via Slack. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import CityList from "../features/cityList/CityList"; | ||
| import Search from "./Search"; | ||
|
|
||
| const App = () => { | ||
| return ( | ||
| <> | ||
| <Search /> | ||
| <CityList /> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default App; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| "use client"; | ||
| import React from "react"; | ||
| import { useDispatch } from 'react-redux'; | ||
| import { useRouter } from 'next/navigation' | ||
| import { deleteCity } from "../features/cityList/cityListSlice"; | ||
| import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-sparklines"; | ||
|
|
||
|
|
||
| function CityForecast({city}) { | ||
|
|
||
| const router = useRouter(); | ||
| const dispatch = useDispatch(); | ||
|
|
||
| const tempArray = city.weatherArrays.temp; | ||
| const pressureArray = city.weatherArrays.pressure; | ||
| const humidityArray = city.weatherArrays.humidity; | ||
| const cityName = (city.name).charAt(0).toUpperCase() + (city.name).slice(1); | ||
|
|
||
| const computeAverage = (array) => { | ||
| if(!Array.isArray(array) || array.length === 0) { | ||
| console.error("Error with array"); | ||
| return 0; | ||
| } | ||
|
|
||
| let sum = array.reduce((a, b) => a + b, 0); | ||
| let avg = (sum / array.length); | ||
| return Math.round(avg); | ||
| }; | ||
|
|
||
| const handleDeleteClick = (id) => { | ||
| dispatch( | ||
| deleteCity(id) | ||
| ); | ||
| router.push('/'); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <ul className='list-group list-group-horizontal text-center' > | ||
| <li className='list-group-item col-3 p-5'> | ||
| <h3>{cityName}</h3> | ||
| <button onClick={() => handleDeleteClick(city.id)} className="btn btn-danger">Delete</button> | ||
| </li> | ||
| <li className='list-group-item col-3'> | ||
| <Sparklines limit={40} width={200} height={100} data={tempArray} > | ||
| <SparklinesLine color="#40c0f5" /> | ||
| <SparklinesReferenceLine type="avg" /> | ||
| </Sparklines> | ||
| <span>{computeAverage(tempArray)} F</span> | ||
| </li> | ||
| <li className='list-group-item col-3'> | ||
| <Sparklines limit={40} width={200} height={100} data={pressureArray} > | ||
| <SparklinesLine color="#d1192e" /> | ||
| <SparklinesReferenceLine type="avg" /> | ||
| </Sparklines> | ||
| <span>{computeAverage(pressureArray)} hPa</span> | ||
| </li> | ||
| <li className='list-group-item col-3'> | ||
| <Sparklines limit={40} width={200} height={100} data={humidityArray} > | ||
| <SparklinesLine color="#8ed53f" /> | ||
| <SparklinesReferenceLine type="avg" /> | ||
| </Sparklines> | ||
| <span>{computeAverage(humidityArray)}%</span> | ||
| </li> | ||
|
Comment on lines
+44
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DRY, this could have been a loop |
||
| </ul> | ||
| </> | ||
| ) | ||
|
|
||
| } | ||
|
|
||
| export default CityForecast; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| "use client"; | ||
| import { useState } from "react"; | ||
| import { useDispatch } from 'react-redux'; | ||
| import { fetchGeoCodes } from "../features/cityList/cityListSlice"; | ||
|
|
||
|
|
||
| const Search = () => { | ||
| const [newSearch, setNewSearch] = useState(""); | ||
| const dispatch = useDispatch(); | ||
|
|
||
| const handleSubmit = (event) => { | ||
| event.preventDefault(); | ||
| dispatch( | ||
| fetchGeoCodes(newSearch)); | ||
|
|
||
| setNewSearch(""); | ||
|
|
||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <div className="d-flex justify-content-center mt-5 col-md-12"> | ||
| <form onSubmit={handleSubmit} className="d-flex"> | ||
| <input | ||
| className="form-control me-2" | ||
| type="text" | ||
| placeholder="Enter City Name" | ||
| value={newSearch} | ||
| onChange={(e) => setNewSearch(e.target.value)} | ||
| /> | ||
| <button type="submit" className="btn btn-primary">Submit</button> | ||
| </form> | ||
| </div> | ||
| </> | ||
| ) | ||
|
|
||
| } | ||
|
|
||
| export default Search; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| "use client"; | ||
| import React, { useEffect } from 'react'; | ||
| import { useSelector, useDispatch } from "react-redux"; | ||
| import CityForecast from '@/app/components/CityForecast'; | ||
| import { fetchForecast } from './cityListSlice'; | ||
|
|
||
|
|
||
| const CityList = () => { | ||
| const cities = useSelector((state) => state.cityList.cityList); | ||
| const status = useSelector((state) => state.cityList.status); | ||
| const error = useSelector((state) => state.cityList.error); | ||
| const dispatch = useDispatch(); | ||
|
|
||
|
|
||
| useEffect(() => { | ||
|
|
||
|
|
||
|
|
||
|
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. check the code spacing and alignment |
||
| cities.forEach((city, index) => { | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for empty lines like these |
||
| dispatch(fetchForecast({lat:city.lat, lon:city.lat, index})); | ||
|
|
||
| }); | ||
|
|
||
| }, [cities.length, dispatch]); | ||
|
|
||
|
|
||
| return ( | ||
| <div className='container mt-5'> | ||
| <div className='row text-center pb-2'> | ||
| <div className='col-3'><h5><>City</></h5></div> | ||
| <div className='col-3'><h5><>Temperature (F)</></h5></div> | ||
| <div className='col-3'><h5>Pressure (hPa)</h5></div> | ||
| <div className='col-3'><h5><>Humidity (%)</></h5></div> | ||
| </div> | ||
|
|
||
| { | ||
| !Array.isArray(cities) || cities.length === 0 && ( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could have made this a function that takes cities for argument and would make the code more readable. |
||
| <p className='mt-5 text-center display-6' >No cities: Enter a city above to see forecast</p> | ||
| ) | ||
| } | ||
| { | ||
| cities?.map((city, index) => ( | ||
|
|
||
| <CityForecast key={index} city={city} index={index} /> | ||
|
|
||
| )) | ||
|
Comment on lines
+43
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. code alignment is off here and makes the code review difficult |
||
| } | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default CityList; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; | ||
| import axios from 'axios'; | ||
|
|
||
| const APIkey = "dd9d476f3e18502da2edd15a3502cd8d"; | ||
|
|
||
|
|
||
| export const fetchGeoCodes = createAsyncThunk('newCity/fetchGeoCodes', async (city, { dispatch }) => { | ||
| const response = await axios.get(`https://api.openweathermap.org/geo/1.0/direct?q=${city}&limit=1&appid=${APIkey}`); | ||
|
|
||
| const newCity = { | ||
| name: city, | ||
| id: Math.floor(Math.random() * 90000000) + 10000000, | ||
| lat: response.data[0].lat, | ||
| lon: response.data[0].lon, | ||
| weatherArrays: {}, | ||
| }; | ||
|
|
||
| dispatch( | ||
| addCity(newCity) | ||
| ); | ||
|
|
||
| } ); | ||
|
|
||
| export const fetchForecast = createAsyncThunk('city/fetchForecast', async ({lat, lon, index}) => { | ||
|
|
||
| const response = await axios.get(`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&units=imperial&appid=${APIkey}`); | ||
|
|
||
| let humidityArray = []; | ||
| let tempArray = []; | ||
| let pressureArray = []; | ||
|
|
||
| for (let i = 0; i < 40; i++) { | ||
| humidityArray[i] = response.data.list[i].main.humidity; | ||
| tempArray[i] = Math.round(response.data.list[i].main.temp); | ||
| pressureArray[i] = response.data.list[i].main.pressure; | ||
| }; | ||
|
|
||
| const forecastArrays = { | ||
| temp: tempArray, | ||
| pressure: pressureArray, | ||
| humidity: humidityArray | ||
| }; | ||
|
|
||
| return {index , arrays: forecastArrays}; | ||
|
|
||
| } ); | ||
|
|
||
| const initialState = { | ||
| cityList: [ | ||
| { | ||
| name: "Chicago", | ||
| id: 84658893, | ||
| lat: 41.8755616, | ||
| lon: -87.6244212, | ||
| weatherArrays: [], | ||
| }, | ||
| { | ||
| name: "Denver", | ||
| id: 66387769, | ||
| lat: 39.7392364, | ||
| lon: -104.984862, | ||
| weatherArrays: [], | ||
| }, | ||
| { | ||
| name: "Nashville", | ||
| id: 31339895, | ||
| lat: 36.1622767, | ||
| lon: -86.7742984, | ||
| weatherArrays: [], | ||
| }, | ||
| ], | ||
|
|
||
| status: 'idle', | ||
| error: null, | ||
| }; | ||
|
|
||
|
|
||
| export const cityListSlice = createSlice({ | ||
| name: "cityList", | ||
| initialState, | ||
| reducers: { | ||
| addCity: (state, action) => { | ||
| state.cityList.push(action.payload); | ||
| }, | ||
| deleteCity: (state, action) => { | ||
| state.cityList = state.cityList.filter(city => city.id !== action.payload); | ||
| }, | ||
| }, | ||
| extraReducers: (builder) => { | ||
| builder | ||
| .addCase(fetchGeoCodes.pending, (state) => { | ||
| state.status = 'loading'; | ||
| }) | ||
| .addCase(fetchGeoCodes.fulfilled, (state, action) => { | ||
| state.status = 'succeeded'; | ||
|
|
||
| }) | ||
| .addCase(fetchGeoCodes.rejected, (state, action) => { | ||
| state.status = 'failed'; | ||
| state.error = action.error.message; | ||
| }) | ||
| .addCase(fetchForecast.pending, (state) => { | ||
| state.status = 'loading'; | ||
| }) | ||
| .addCase(fetchForecast.fulfilled, (state, action) => { | ||
| state.status = 'succeeded'; | ||
| const { index , arrays} = action.payload; | ||
| const cityToUpdate = state.cityList[index]; | ||
| if (cityToUpdate) { | ||
| cityToUpdate.weatherArrays = arrays | ||
| } | ||
|
|
||
| }) | ||
| .addCase(fetchForecast.rejected, (state, action) => { | ||
| state.status = 'failed'; | ||
| state.error = action.error.message; | ||
| }); | ||
| }, | ||
| }); | ||
|
|
||
| export const { addCity, deleteCity } = cityListSlice.actions; | ||
| export default cityListSlice.reducer; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,18 @@ | ||
| import './globals.css' | ||
| import { Inter } from 'next/font/google' | ||
| "use client"; | ||
| import 'bootstrap/dist/css/bootstrap.min.css'; | ||
| import { Inter } from 'next/font/google'; | ||
| import { Provider } from 'react-redux'; | ||
| import store from './store'; | ||
|
|
||
| const inter = Inter({ subsets: ['latin'] }) | ||
|
|
||
| export const metadata = { | ||
| title: 'Create Next App', | ||
| description: 'Generated by create next app', | ||
| } | ||
|
|
||
| export default function RootLayout({ children }) { | ||
| return ( | ||
| <html lang="en"> | ||
| <body className={inter.className}>{children}</body> | ||
| <body className={inter.className}> | ||
| <Provider store={store}>{children}</Provider> | ||
| </body> | ||
| </html> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be const, not let