diff --git a/app/components/WeatherApp.js b/app/components/WeatherApp.js new file mode 100644 index 0000000..adb2646 --- /dev/null +++ b/app/components/WeatherApp.js @@ -0,0 +1,96 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { fetchWeatherData, setCity } from "app/redux/weatherSlice.js"; +import WeatherChart from "./WeatherChart"; + +function WeatherApp() { + const dispatch = useDispatch(); + const { city, weatherData, loading, error } = useSelector( + (state) => state.weather + ); + const [inputCity, setInputCity] = useState(city); + + useEffect(() => { + if (city) { + dispatch(fetchWeatherData(city)); + } + }, [city, dispatch]); + + const handleInputChange = (e) => { + setInputCity(e.target.value); + }; + + const handleFormSubmit = (e) => { + e.preventDefault(); + dispatch(setCity(inputCity)); + setInputCity('') + }; + + if (loading) return

Fetching weather data for {city}...

; + if (error) return

Failed to fetch weather data for {city} {error}

; + + const temperatureData = weatherData + ? weatherData.list.map((item) => { + console.log(item) + const celsius = item.main.temp + const fahrenheit = (celsius * (9 / 5) + 32); // Convert Celsius to Fahrenheit + + return fahrenheit + }) + : []; + const pressureData = weatherData + ? weatherData.list.map((item) => item.main.pressure) + : []; + const humidityData = weatherData + ? weatherData.list.map((item) => item.main.humidity) + : []; + + return ( + <> +

Weather Data

+
+ + +
+ + {weatherData && ( + <> +
+
+ + + + +
+
+ + )} + + ); +} + +export default WeatherApp; diff --git a/app/components/WeatherChart.js b/app/components/WeatherChart.js new file mode 100644 index 0000000..9586d0b --- /dev/null +++ b/app/components/WeatherChart.js @@ -0,0 +1,53 @@ +"use client"; + +import React from "react"; +import { + Sparklines, + SparklinesLine, + SparklinesReferenceLine, +} from "react-sparklines"; + +function calculateAverage(data) { + if (!data || data.length === 0) { + return 0; // Return 0 for empty or invalid data + } + + let sum = 0; + let validCount = 0; + for (const point of data) { + if (typeof point === "number" && !isNaN(point)) { + sum += point; + validCount++; + } else { + console.warn("Invalid data point:", point); + } + + } + + if (validCount === 0) { + return 0; // Return 0 if no valid data points + } + + return sum / validCount; +} + +function WeatherChart({ data, title, units, color }) { + const average = calculateAverage(data) + console.log(average) + return ( +
+

+ {title} ({units}) +

+ + + + +

+ Average: {average.toFixed(2)} {units} +

+
+ ); +} + +export default WeatherChart; diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/app/favicon.ico and /dev/null differ diff --git a/app/globals.css b/app/globals.css index d4f491e..99c4a7e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,107 +1,84 @@ :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; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); + background-color: #000000; + color: blanchedalmond; + font-family: "Lucida Console", Monaco, monospace; + font-size: 16px; + letter-spacing: 2px; + word-spacing: 2px; + color: #000000; + font-weight: normal; + text-decoration: none; + font-style: normal; + font-variant: small-caps; + text-transform: uppercase; +} - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); +.content { + max-width: 600px; + margin: 20px auto; + padding: 20px; + border: 3px solid #ccc; + border-radius: 5px; +} - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; +.loading, +.error { + text-align: center; + margin-top: 20px; } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; +.error { + color: red; +} - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); +.form { + margin-bottom: 20px; + text-align: center; +} - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); +.heading { + text-align: center; + margin-bottom: 20px; + color: #595757; +} - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } +.btn { + margin-top: 20px; + padding: 4px 10px; + background-color: #425263; + color: white; + border: none; + border-radius: 3px; + cursor: pointer; } -* { - box-sizing: border-box; - padding: 0; - margin: 0; +.weatherChart { + padding: 20px; + border: 1px solid #595757; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background-color: #e2e1e1; } -html, -body { - max-width: 100vw; - overflow-x: hidden; +.chartHeading { + margin-bottom: 10px; + text-align: center; + color: #333; } -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); +Sparklines { + height: 120px; + width: 200px; } -a { - color: inherit; - text-decoration: none; +.avg { + margin-top: 10px; + text-align: center; + color: #555; } -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } +.chartsDiv { + display: flex; + flex-direction: column; + gap: 20px; } diff --git a/app/page.js b/app/page.js index f049c39..ccc7a5b 100644 --- a/app/page.js +++ b/app/page.js @@ -1,95 +1,13 @@ -import Image from 'next/image' -import styles from './page.module.css' +'use client' +import React from 'react'; +import { Provider } from 'react-redux'; +import store from "app/redux/store.js"; +import WeatherApp from "./components/WeatherApp.js"; export default function Home() { return ( -
-
-

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

-
- - By{' '} - Vercel Logo - -
-
- -
- Next.js Logo -
- -
- -

- 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. -

-
-
-
+ + + ) } diff --git a/app/redux/store.js b/app/redux/store.js new file mode 100644 index 0000000..8175c38 --- /dev/null +++ b/app/redux/store.js @@ -0,0 +1,12 @@ +import { configureStore } from "@reduxjs/toolkit"; +import weatherReducer from "app/redux/weatherSlice.js"; + + + +const store = configureStore({ + reducer: { + weather: weatherReducer + } +}) + +export default store; \ No newline at end of file diff --git a/app/redux/weatherSlice.js b/app/redux/weatherSlice.js new file mode 100644 index 0000000..6293a00 --- /dev/null +++ b/app/redux/weatherSlice.js @@ -0,0 +1,60 @@ +import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; + +const API_KEY = process.env.NEXT_PUBLIC_OPENWEATHER_API_KEY +const API_URL = "https://api.openweathermap.org/data/2.5/forecast"; + +export const fetchWeatherData = createAsyncThunk( + "weather/fetchWeatherData", + async (city) => { + try { + const apiUrl = `${API_URL}?q=${city}&appid=${API_KEY}&units=metric`; + + const response = await fetch(apiUrl); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + return Promise.reject(error.message); + } + } +); + +const initialState = { + city: "", + weatherData: null, + loading: false, + error: null, +}; + +const weatherSlice = createSlice({ + name: "weather", + initialState, + reducers: { + setCity: (state, action) => { + state.city = action.payload; + state.error = null; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchWeatherData.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchWeatherData.fulfilled, (state, action) => { + state.loading = false; + state.weatherData = action.payload; + }) + .addCase(fetchWeatherData.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message; + }); + }, +}); + +export const { setCity } = weatherSlice.actions; +export default weatherSlice.reducer; diff --git a/package-lock.json b/package-lock.json index 90f6bb1..5a175d9 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.5.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.5.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", + "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==", + "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..505d50f 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,13 @@ "lint": "next lint" }, "dependencies": { + "@reduxjs/toolkit": "^2.5.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" } }