diff --git a/app/components/CityWeather.js b/app/components/CityWeather.js new file mode 100644 index 0000000..7aad112 --- /dev/null +++ b/app/components/CityWeather.js @@ -0,0 +1,35 @@ +import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-sparklines"; +import "bootstrap/dist/css/bootstrap.min.css"; + +const CityWeather = ({ city }) => { + return ( + + +
{city.name}
+ + + + + + +

{city.avgTemp} °F

+ + + + + + +

{city.avgPressure} hPa

+ + + + + + +

{city.avgHumidity} %

+ + + ); +}; + +export default CityWeather; diff --git a/app/layout.js b/app/layout.js index c93f806..08a807f 100644 --- a/app/layout.js +++ b/app/layout.js @@ -1,17 +1,15 @@ -import './globals.css' -import { Inter } from 'next/font/google' - -const inter = Inter({ subsets: ['latin'] }) - -export const metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} +'use client'; +import { Inter } from 'next/font/google'; +import { Provider } from 'react-redux'; +import store from './store/configureStore'; +const inter = Inter({ subsets: ['latin'] }); export default function RootLayout({ children }) { - return ( - - {children} - - ) -} + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/app/page.js b/app/page.js index f049c39..becde07 100644 --- a/app/page.js +++ b/app/page.js @@ -1,95 +1,62 @@ -import Image from 'next/image' -import styles from './page.module.css' +"use client"; +import { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { addCity } from "./store/slices/cities"; +import CityWeather from "./components/CityWeather"; +import "bootstrap/dist/css/bootstrap.min.css"; export default function Home() { - return ( -
-
-

- Get started by editing  - app/page.js -

-
- - By{' '} - Vercel Logo - -
-
+ const cities = useSelector((state) => state.cities.cities); + const dispatch = useDispatch(); + const [newCity, setNewCity] = useState(""); -
- Next.js Logo { + if (!cities.some((city) => city.name.toLowerCase() === newCity.toLowerCase())) { + dispatch(addCity(newCity)); + } + setNewCity(""); +}; + + return ( +
+

Weather Forecast

+
+ setNewCity(e.target.value)} + placeholder="Add a city" + className="form-control" /> +
- -
- -

- Docs -> -

-

Find in-depth information about Next.js features and API.

-
- - -

- Learn -> -

-

Learn about Next.js in an interactive course with quizzes!

-
- - -

- Templates -> -

-

Explore the Next.js 13 playground.

-
- - -

- Deploy -> -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
+
+ + + + + + + + + + + {cities.map((city, index) => ( + + ))} + +
+ City + + Temp + + Pressure + + Humidity +
-
- ) + + ); } diff --git a/app/store/configureStore.js b/app/store/configureStore.js new file mode 100644 index 0000000..9afb8ce --- /dev/null +++ b/app/store/configureStore.js @@ -0,0 +1,8 @@ +import { configureStore } from "@reduxjs/toolkit"; +import rootReducer from "./rootReducer"; + +const store = configureStore({ + reducer: rootReducer, +}); + +export default store; diff --git a/app/store/rootReducer.js b/app/store/rootReducer.js new file mode 100644 index 0000000..7eeccf1 --- /dev/null +++ b/app/store/rootReducer.js @@ -0,0 +1,8 @@ +import { combineReducers } from "redux"; +import citiesReducer from "./slices/cities"; + +const rootReducer = combineReducers({ + cities: citiesReducer, +}); + +export default rootReducer; diff --git a/app/store/slices/cities.js b/app/store/slices/cities.js new file mode 100644 index 0000000..11596a1 --- /dev/null +++ b/app/store/slices/cities.js @@ -0,0 +1,67 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; + +const apiKey = ""; + +export const addCity = createAsyncThunk( + "city/fetchCityWeather", + async (cityName) => { + const response = await fetch( + `https://api.openweathermap.org/data/2.5/forecast?q=${cityName}&appid=${apiKey}&units=imperial` + ); + const data = await response.json(); + + const avgTemp = + Math.round(data.list.reduce( + (acc, threeHour) => acc + threeHour.main.temp, + 0 + ) / data.list.length); + + const avgHumidity = + Math.round(data.list.reduce( + (acc, threeHour) => acc + threeHour.main.humidity, + 0 + ) / data.list.length); + + const avgPressure = + Math.round(data.list.reduce( + (acc, threeHour) => acc + threeHour.main.pressure, + 0 + ) / data.list.length); + + return { + name: data.city.name, + temp: data.list.map((threeHour) => threeHour.main.temp), + pressure: data.list.map((threeHour) => threeHour.main.pressure), + humidity: data.list.map((threeHour) => threeHour.main.humidity), + avgTemp: avgTemp, + avgHumidity: avgHumidity, + avgPressure: avgPressure, + }; + } +); + +const citySlice = createSlice({ + name: "city", + initialState: { + cities: [], + status: "idle", + error: null, + }, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(addCity.pending, (state) => { + state.status = "loading"; + }) + .addCase(addCity.fulfilled, (state, action) => { + state.status = "fulfilled"; + state.cities.push(action.payload); + }) + .addCase(addCity.rejected, (state, action) => { + state.status = "rejected"; + state.error = action.error.message; + }); + }, +}); + +export default citySlice.reducer; diff --git a/package-lock.json b/package-lock.json index 90f6bb1..0d3d11e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,14 @@ "name": "parsity_rtk_weather", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^2.6.1", "eslint": "8.50.0", "eslint-config-next": "13.5.3", "next": "13.5.3", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "react-redux": "^9.2.0", + "react-sparklines": "^1.7.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -296,6 +299,30 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.1.tgz", + "integrity": "sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.0.tgz", @@ -314,6 +341,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/parser": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", @@ -1759,6 +1792,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2683,6 +2726,57 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-sparklines": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz", + "integrity": "sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -2723,6 +2817,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -3225,6 +3325,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 6ca0f8f..6eb0e21 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,13 @@ "lint": "next lint" }, "dependencies": { + "@reduxjs/toolkit": "^2.6.1", "eslint": "8.50.0", "eslint-config-next": "13.5.3", "next": "13.5.3", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "react-redux": "^9.2.0", + "react-sparklines": "^1.7.0" } }