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
13 changes: 12 additions & 1 deletion README.md
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.
13 changes: 13 additions & 0 deletions app/components/App.jsx
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;
71 changes: 71 additions & 0 deletions app/components/CityForecast.jsx
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);
Comment on lines +25 to +26

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

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

Choose a reason for hiding this comment

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

DRY, this could have been a loop

</ul>
</>
)

}

export default CityForecast;
39 changes: 39 additions & 0 deletions app/components/Search.jsx
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;
53 changes: 53 additions & 0 deletions app/features/cityList/CityList.jsx
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

Choose a reason for hiding this comment

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

check the code spacing and alignment

cities.forEach((city, index) => {

Choose a reason for hiding this comment

The 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 && (

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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;
122 changes: 122 additions & 0 deletions app/features/cityList/cityListSlice.js
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;
6 changes: 3 additions & 3 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
"Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
"Fira Mono", "Droid Sans Mono", "Courier New", monospace;

--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
Expand Down
15 changes: 8 additions & 7 deletions app/layout.js
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>
)
}
Loading