From 8895e33bd6cf1b5a4a087c404a64574fd82f0249 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 8 Aug 2024 14:03:25 -0400 Subject: [PATCH 01/18] set up api calls --- .vscode/settings.json | 3 + app/components/AppNavbar.jsx | 67 +++ app/components/BootstrapClient.js | 15 + app/components/SearchBar.jsx | 52 ++ app/components/WeatherAPI.js | 120 ++++ app/globals.css | 97 ---- app/layout.js | 22 +- app/page.js | 120 ++-- app/page.module.css | 229 -------- app/store/StoreProvider.js | 12 + app/store/configureStore.js | 9 + app/store/rootReducer.js | 9 + app/store/slices/locations.js | 22 + package-lock.json | 885 ++++++++++++++++++++++++++---- package.json | 13 +- 15 files changed, 1136 insertions(+), 539 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 app/components/AppNavbar.jsx create mode 100644 app/components/BootstrapClient.js create mode 100644 app/components/SearchBar.jsx create mode 100644 app/components/WeatherAPI.js create mode 100644 app/store/StoreProvider.js create mode 100644 app/store/configureStore.js create mode 100644 app/store/rootReducer.js create mode 100644 app/store/slices/locations.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..884e0d7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["APIKEY"] +} diff --git a/app/components/AppNavbar.jsx b/app/components/AppNavbar.jsx new file mode 100644 index 0000000..40effdd --- /dev/null +++ b/app/components/AppNavbar.jsx @@ -0,0 +1,67 @@ +"use client" +import { useSelector, useDispatch } from "react-redux"; +import { setCurrentLocation } from "../store/slices/locations"; + + +export default function AppNavbar() { + + const currentLocation = useSelector((state) => {state.locations.currentLocation}) + + const dispatch = useDispatch(); + + const handleSetLocation = () => { + console.log("Handling Location"); + const geolocation = navigator.geolocation; + if (!geolocation) return alert("Geolocation is not supported. Sorry!"); + + const onSuccess = (data) => { + console.log(data); + const { latitude, longitude } = data.coords; + console.log({latitude, longitude}) + dispatch(setCurrentLocation({latitude, longitude})); + }; + const onError = (error) => { + console.log(error) + }; + + geolocation.getCurrentPosition(onSuccess, onError, {enableHighAccuracy:true}); + + }; + + + return ( + + ) +} \ No newline at end of file diff --git a/app/components/BootstrapClient.js b/app/components/BootstrapClient.js new file mode 100644 index 0000000..1daddf9 --- /dev/null +++ b/app/components/BootstrapClient.js @@ -0,0 +1,15 @@ +"use client"; + +import { useEffect } from "react"; + +function BootstrapClient() { + useEffect(() => { + require('bootstrap/dist/js/bootstrap.bundle.min.js'); + }, []); + + return null; +} + +export default BootstrapClient; + + diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx new file mode 100644 index 0000000..161b8c2 --- /dev/null +++ b/app/components/SearchBar.jsx @@ -0,0 +1,52 @@ +"use client" + +import { useState } from "react"; +import { getWeather } from "./WeatherAPI"; +import { useDispatch } from "react-redux"; +import { pushLocation } from "../store/slices/locations"; + + +export default function SearchBar() { + const [inputText, setInputText] = useState(""); + const dispatch = useDispatch(); + + const handleSubmit = async (e) => { + e.preventDefault(); + console.log("submitting ", inputText); + // todo : Validate city + + // Get location based on city name + const weather = await getWeather(inputText); + + // update redux + dispatch(pushLocation(weather)); + } + + + return ( +
+
+
+
handleSubmit(e)}> +
+ setInputText(target.value)} + /> + +
+
+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js new file mode 100644 index 0000000..0e485b9 --- /dev/null +++ b/app/components/WeatherAPI.js @@ -0,0 +1,120 @@ +import { v4 as uuidv4 } from 'uuid'; + +const APIKEY = "deecee58f4daa55a503c09ae97c1d3ab"; +const fetchData = async (url) => { + const resp = await fetch(url); + const data = await resp.json(); + return data; +} + +const getLatLonData = async function (cityName) { + const url = `http://api.openweathermap.org/geo/1.0/direct?q=${cityName}&limit=1&appid=${APIKEY}`; + const data = await fetchData(url); + + if(data.length === 0) return {}; + + const {name, lat, lon} = data[0]; + + return {name, lat, lon}; +}; + +const getCurrentWeatherData = async function (lonLatData, unit="imperial") { + const {lon, lat, name,} = lonLatData; + const returnData = {} + const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=${unit}&appid=${APIKEY}` + const data = await fetchData(url); + returnData.name = data.name; + returnData.temp = data.main.temp; + const {description, icon} = data.weather[0]; + const weatherData = {...returnData, weather: description, iconCode: icon}; + return weatherData; +}; + +const getFiveDayWeatherData = async function (lonLatData, units="imperial") { + const {lon, lat, name,} = lonLatData; + + const url = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&units=${units}&appid=${APIKEY}`; + const data = await fetchData(url); + + // todo: refactor formatFiveDayData function + return formatFiveDayData(data.list); +}; + +const formatFiveDayData = function (data) { + let dateSeparatedArr = []; + let dateArr = [] + + /** + * Formatting logic: + * Loop through data array (40 items where every 8 items is 1 day) while keep track of number items traversed + * We append each item to a temp array (dateArr) + * Everytime we've traversed 8 items (1 day) we push the temp array to the output array (dateSeparatedArr) + */ + data.forEach((itm, index) => { + const itmCt = index + 1; + // if itemCt is devisable by 8, it means it is the last itm for that day + if ((itmCt % 8) === 0){ + dateArr.push(itm); + dateSeparatedArr.push(dateArr); + dateArr = []; + return + } + dateArr.push(itm); + return + }); + + /** + * Formatting logic: + * Now that we have an array where each day is an index inside an array, + * the goal now is to reduce each index in the day ARRAY to be a single day OBJECT + * on every loop we += item temp to the temp of the accumulator (which is an obj), to be averaged later + * The first index of the day is used to get the day of the week and weather condition + * The last index of the day is used to calculated the average temp of that day + * + * from: dateSeparatedArray = [[day1data, day1data, day1data], [day2data, day2data], ...] + * to: dateSeparatedArray = [{day1Obj}, {day2Obj}, ...] + */ + dateSeparatedArr = dateSeparatedArr.map((dayArr) => { + + return dayArr.reduce((accumulator, itm, dayIndex) => { + accumulator.temp += itm.main.temp; + + // get Day of week, weather icon, and weather condition + if (dayIndex === 0) { + const datesStrings = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + const date = new Date(itm.dt_txt); + const dayOfWk = date.getDay(); + accumulator.day = datesStrings[dayOfWk]; + accumulator.weather = itm.weather[0].description; + accumulator.iconCode = itm.weather[0].icon; + }; + + // if item is last item of day calc avg temp; + if (dayIndex === 7) { + const avgTemp = accumulator.temp / 8; + const roundedTemp = Math.ceil(avgTemp * 100) / 100; + accumulator.temp = roundedTemp; + }; + return accumulator; + }, {"temp": 0, "weather": "", "iconCode": "", "day":""}) + }) + return dateSeparatedArr; +}; + +const getWeather = async function (cityName) { + const latLonData = await getLatLonData(cityName); + if (Object.keys(latLonData).length === 0) { + return {} + } + const currentWeatherData = await getCurrentWeatherData(latLonData); + const FiveDayWeatherData = await getFiveDayWeatherData(latLonData); + + return { + id: uuidv4(), + name: latLonData.name, + currentWeather: currentWeatherData, + fiveDayWeather: FiveDayWeatherData, + } +} + +export { getWeather } \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index d4f491e..e3ba175 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,78 +1,3 @@ -: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) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --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) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} - * { box-sizing: border-box; padding: 0; @@ -82,26 +7,4 @@ html, body { max-width: 100vw; - overflow-x: hidden; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } } diff --git a/app/layout.js b/app/layout.js index c93f806..01b1340 100644 --- a/app/layout.js +++ b/app/layout.js @@ -1,5 +1,9 @@ -import './globals.css' -import { Inter } from 'next/font/google' +import './globals.css'; +import { Inter } from 'next/font/google'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import BootstrapClient from './components/BootstrapClient.js'; +import AppNavbar from './components/AppNavbar'; +import StoreProvider from './store/StoreProvider'; const inter = Inter({ subsets: ['latin'] }) @@ -10,8 +14,16 @@ export const metadata = { export default function RootLayout({ children }) { return ( - - {children} - + <> + + + + + {children} + + + + + ) } diff --git a/app/page.js b/app/page.js index f049c39..81f7707 100644 --- a/app/page.js +++ b/app/page.js @@ -1,95 +1,49 @@ +"use client" import Image from 'next/image' import styles from './page.module.css' +import SearchBar from './components/SearchBar' export default function Home() { return ( -
-
-

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

-
- - By{' '} - Vercel Logo - +
+
+ +
+
+
+
+
Modal title
+ +
+
+

Modal body text goes here.

+
+
+ +
+
+
-
- -
- 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/page.module.css b/app/page.module.css index 6676d2c..e69de29 100644 --- a/app/page.module.css +++ b/app/page.module.css @@ -1,229 +0,0 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - min-height: 100vh; -} - -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - max-width: 100%; - width: var(--max-width); -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ''; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo { - position: relative; -} -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .card:hover span { - transform: none; - } -} - -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); - } -} diff --git a/app/store/StoreProvider.js b/app/store/StoreProvider.js new file mode 100644 index 0000000..438054e --- /dev/null +++ b/app/store/StoreProvider.js @@ -0,0 +1,12 @@ +"use client" +import store from './configureStore'; +import { Provider } from 'react-redux'; + +export default function StoreProvider({children}) { + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/app/store/configureStore.js b/app/store/configureStore.js new file mode 100644 index 0000000..f913f61 --- /dev/null +++ b/app/store/configureStore.js @@ -0,0 +1,9 @@ +"use client" +import { configureStore } from "@reduxjs/toolkit"; +import rootReducer from './rootReducer'; + +const store = configureStore({ + reducer: rootReducer +}); + +export default store; \ No newline at end of file diff --git a/app/store/rootReducer.js b/app/store/rootReducer.js new file mode 100644 index 0000000..af56b58 --- /dev/null +++ b/app/store/rootReducer.js @@ -0,0 +1,9 @@ +"use client" +import { combineReducers } from "redux"; +import locationsReducer from "./slices/locations"; + +const rootReducer = combineReducers({ + locations: locationsReducer +}); + +export default rootReducer; diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js new file mode 100644 index 0000000..64257a9 --- /dev/null +++ b/app/store/slices/locations.js @@ -0,0 +1,22 @@ +import { createSlice, current } from "@reduxjs/toolkit"; + +export const locationsSlice = createSlice({ + name: "locations", + initialState: { + currentLocation: null, + locations: [], + activeLocation: null, + }, + reducers: { + setCurrentLocation: (state, action) => { + state.currentLocation = action.payload; + }, + pushLocation: (state, action) => { + state.locations.push(action.payload); + } + } +}) + +export const { setCurrentLocation, pushLocation } = locationsSlice.actions; + +export default locationsSlice.reducer; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 90f6bb1..69aca92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,16 @@ "name": "parsity_rtk_weather", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^2.2.7", + "bootstrap": "^5.3.3", "eslint": "8.50.0", - "eslint-config-next": "13.5.3", - "next": "13.5.3", - "react": "18.2.0", - "react-dom": "18.2.0" + "eslint-config-next": "^14.2.5", + "next": "^14.2.5", + "react": "^18.3.1", + "react-bootstrap": "^2.10.4", + "react-dom": "^18.3.1", + "react-redux": "^9.1.2", + "uuid": "^10.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -24,9 +29,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -116,23 +121,107 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@next/env": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.3.tgz", - "integrity": "sha512-X4te86vsbjsB7iO4usY9jLPtZ827Mbx+WcwNBGUOIuswuTAKQtzsuoxc/6KLxCMvogKG795MhrR1LDhYgDvasg==" + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", + "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==" }, "node_modules/@next/eslint-plugin-next": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-13.5.3.tgz", - "integrity": "sha512-lbZOoEjzSuTtpk9UgV9rOmxYw+PsSfNR+00mZcInqooiDMZ1u+RqT1YQYLsEZPW1kumZoQe5+exkCBtZ2xn0uw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.5.tgz", + "integrity": "sha512-LY3btOpPh+OTIpviNojDpUdIbHW9j0JBYBjsIp8IxtDFfYFyORvw3yNq6N231FVqQA7n7lwaf7xHbVJlA1ED7g==", + "dependencies": { + "glob": "10.3.10" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dependencies": { - "glob": "7.1.7" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.3.tgz", - "integrity": "sha512-6hiYNJxJmyYvvKGrVThzo4nTcqvqUTA/JvKim7Auaj33NexDqSNwN5YrrQu+QhZJCIpv2tULSHt+lf+rUflLSw==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", + "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", "cpu": [ "arm64" ], @@ -145,9 +234,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.3.tgz", - "integrity": "sha512-UpBKxu2ob9scbpJyEq/xPgpdrgBgN3aLYlxyGqlYX5/KnwpJpFuIHU2lx8upQQ7L+MEmz+fA1XSgesoK92ppwQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", + "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", "cpu": [ "x64" ], @@ -160,9 +249,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.3.tgz", - "integrity": "sha512-5AzM7Yx1Ky+oLY6pHs7tjONTF22JirDPd5Jw/3/NazJ73uGB05NqhGhB4SbeCchg7SlVYVBeRMrMSZwJwq/xoA==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", + "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", "cpu": [ "arm64" ], @@ -175,9 +264,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.3.tgz", - "integrity": "sha512-A/C1shbyUhj7wRtokmn73eBksjTM7fFQoY2v/0rTM5wehpkjQRLOXI8WJsag2uLhnZ4ii5OzR1rFPwoD9cvOgA==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", + "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", "cpu": [ "arm64" ], @@ -190,9 +279,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.3.tgz", - "integrity": "sha512-FubPuw/Boz8tKkk+5eOuDHOpk36F80rbgxlx4+xty/U71e3wZZxVYHfZXmf0IRToBn1Crb8WvLM9OYj/Ur815g==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", + "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", "cpu": [ "x64" ], @@ -205,9 +294,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.3.tgz", - "integrity": "sha512-DPw8nFuM1uEpbX47tM3wiXIR0Qa+atSzs9Q3peY1urkhofx44o7E1svnq+a5Q0r8lAcssLrwiM+OyJJgV/oj7g==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", + "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", "cpu": [ "x64" ], @@ -220,9 +309,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.3.tgz", - "integrity": "sha512-zBPSP8cHL51Gub/YV8UUePW7AVGukp2D8JU93IHbVDu2qmhFAn9LWXiOOLKplZQKxnIPUkJTQAJDCWBWU4UWUA==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", + "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", "cpu": [ "arm64" ], @@ -235,9 +324,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.3.tgz", - "integrity": "sha512-ONcL/lYyGUj4W37D4I2I450SZtSenmFAvapkJQNIJhrPMhzDU/AdfLkW98NvH1D2+7FXwe7yclf3+B7v28uzBQ==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", + "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", "cpu": [ "ia32" ], @@ -250,9 +339,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.3.tgz", - "integrity": "sha512-2Vz2tYWaLqJvLcWbbTlJ5k9AN6JD7a5CN2pAeIzpbecK8ZF/yobA39cXtv6e+Z8c5UJuVOmaTldEAIxvsIux/Q==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", + "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", "cpu": [ "x64" ], @@ -296,16 +385,116 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz", + "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", + "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "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", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.8.0.tgz", + "integrity": "sha512-xJEOXUOTmT4FngTmhdjKFRrVVF0hwCLNPdatLCHkyS4dkiSK12cEu1Y0fjxktjJrdst9jJIc5J6ihMJCoWEN/g==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.4.9", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.0.tgz", "integrity": "sha512-EF3948ckf3f5uPgYbQ6GhyA56Dmv8yg0+ir+BroRjwdxyZJsekhZzawOecC2rOTPCz173t7ZcR1HHZu0dZgOCw==" }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -314,6 +503,38 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, "node_modules/@typescript-eslint/parser": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", @@ -647,6 +868,24 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -699,9 +938,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001541", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz", - "integrity": "sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==", + "version": "1.0.30001649", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", + "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", "funding": [ { "type": "opencollective", @@ -732,6 +971,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -771,6 +1015,11 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -856,6 +1105,20 @@ "node": ">=6.0.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1048,13 +1311,13 @@ } }, "node_modules/eslint-config-next": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-13.5.3.tgz", - "integrity": "sha512-VN2qbCpq2DMWgs7SVF8KTmc8bVaWz3s4nmcFqRLs7PNBt5AXejOhJuZ4zg2sCEHOvz5RvqdwLeI++NSCV6qHVg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.5.tgz", + "integrity": "sha512-zogs9zlOiZ7ka+wgUnmcM0KBEDjo4Jis7kxN1jvC0N4wynQ2MIx/KBkg4mVF63J5EK4W0QMCn7xO3vNisjaAoA==", "dependencies": { - "@next/eslint-plugin-next": "13.5.3", + "@next/eslint-plugin-next": "14.2.5", "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.28.1", @@ -1499,6 +1762,21 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1604,11 +1882,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, "node_modules/globals": { "version": "13.22.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", @@ -1759,6 +2032,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "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", @@ -1809,6 +2091,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -1917,6 +2207,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -2120,6 +2418,23 @@ "set-function-name": "^2.0.1" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2289,15 +2604,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -2317,38 +2640,38 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/next": { - "version": "13.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.3.tgz", - "integrity": "sha512-4Nt4HRLYDW/yRpJ/QR2t1v63UOMS55A38dnWv3UDOWGezuY0ZyFO1ABNbD7mulVzs9qVhgy2+ppjdsANpKP1mg==", + "version": "14.2.5", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", + "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", "dependencies": { - "@next/env": "13.5.3", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.5", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", - "postcss": "8.4.14", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0", - "zod": "3.21.4" + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.14.0" + "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.3", - "@next/swc-darwin-x64": "13.5.3", - "@next/swc-linux-arm64-gnu": "13.5.3", - "@next/swc-linux-arm64-musl": "13.5.3", - "@next/swc-linux-x64-gnu": "13.5.3", - "@next/swc-linux-x64-musl": "13.5.3", - "@next/swc-win32-arm64-msvc": "13.5.3", - "@next/swc-win32-ia32-msvc": "13.5.3", - "@next/swc-win32-x64-msvc": "13.5.3" + "@next/swc-darwin-arm64": "14.2.5", + "@next/swc-darwin-x64": "14.2.5", + "@next/swc-linux-arm64-gnu": "14.2.5", + "@next/swc-linux-arm64-musl": "14.2.5", + "@next/swc-linux-x64-gnu": "14.2.5", + "@next/swc-linux-x64-musl": "14.2.5", + "@next/swc-win32-arm64-msvc": "14.2.5", + "@next/swc-win32-ia32-msvc": "14.2.5", + "@next/swc-win32-x64-msvc": "14.2.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -2357,6 +2680,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -2563,6 +2889,26 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2572,9 +2918,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -2588,9 +2934,9 @@ } }, "node_modules/postcss": { - "version": "8.4.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", - "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -2599,10 +2945,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -2628,6 +2978,18 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -2656,9 +3018,9 @@ ] }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -2666,16 +3028,45 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.4.tgz", + "integrity": "sha512-W3398nBM2CBfmGP2evneEO3ZZwEMPtHs72q++eNw60uDGDAdiGn0f9yNys91eo7/y8CTF5Ke1C0QO8JFVPU40Q==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.6.9", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-is": { @@ -2683,6 +3074,61 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "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==" + }, + "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==", + "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 +3169,11 @@ "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==" + }, "node_modules/resolve": { "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", @@ -2831,9 +3282,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -2897,6 +3348,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -2906,9 +3368,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -2921,6 +3383,66 @@ "node": ">=10.0.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -2993,6 +3515,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -3103,9 +3637,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/type-check": { "version": "0.4.0", @@ -3217,6 +3751,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3225,16 +3773,32 @@ "punycode": "^2.1.0" } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" + "loose-envify": "^1.0.0" } }, "node_modules/which": { @@ -3323,6 +3887,93 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3343,14 +3994,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 6ca0f8f..5fe4e85 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,15 @@ "lint": "next lint" }, "dependencies": { + "@reduxjs/toolkit": "^2.2.7", + "bootstrap": "^5.3.3", "eslint": "8.50.0", - "eslint-config-next": "13.5.3", - "next": "13.5.3", - "react": "18.2.0", - "react-dom": "18.2.0" + "eslint-config-next": "^14.2.5", + "next": "^14.2.5", + "react": "^18.3.1", + "react-bootstrap": "^2.10.4", + "react-dom": "^18.3.1", + "react-redux": "^9.1.2", + "uuid": "^10.0.0" } } From 66bfeb496d63e30e96495a6b3ef292d25e74e9f3 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 8 Aug 2024 16:36:10 -0400 Subject: [PATCH 02/18] added search result validation --- app/components/SearchBar.jsx | 85 +++++++++++++++++++++++++++--------- app/components/WeatherAPI.js | 1 + app/page.js | 5 +++ package-lock.json | 65 ++++++++++++++++++++++++++- package.json | 5 ++- 5 files changed, 139 insertions(+), 22 deletions(-) diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index 161b8c2..70c8c9e 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -4,19 +4,55 @@ import { useState } from "react"; import { getWeather } from "./WeatherAPI"; import { useDispatch } from "react-redux"; import { pushLocation } from "../store/slices/locations"; +import { useForm } from "react-hook-form"; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; export default function SearchBar() { const [inputText, setInputText] = useState(""); + const [searchError, setSearchError] = useState(false); const dispatch = useDispatch(); + yup.setLocale({ + mixed: { + required: 'Required Field', + }, + }); + const weatherSchema = yup + .object({ + id: yup.string().required(), + name: yup.string().required(), + latLon: yup.object().required(), + currentWeather: yup.object().required(), + fiveDayWeather: yup.array().required() + }); + const formSchema = yup + .object({ + city: yup.string().required().min(5), + }); - const handleSubmit = async (e) => { - e.preventDefault(); + const { + register, + handleSubmit, + formState: { errors } + } = useForm({ + resolver: yupResolver(formSchema) + }); + + const handleFormSubmit = async () => { + console.log(errors) console.log("submitting ", inputText); - // todo : Validate city - // Get location based on city name const weather = await getWeather(inputText); + console.log(weather) + + // Validate results + if (Object.keys(weather).length === 0) { + setSearchError(true); + return; + } else { + setSearchError(false); + } // update redux dispatch(pushLocation(weather)); @@ -27,26 +63,35 @@ export default function SearchBar() {
-
handleSubmit(e)}> -
- setInputText(target.value)} - /> - + +
+
+ setInputText(target.value)} + /> + +
+ {errors.city && +
+

{errors.city.message}

+
+ } + {searchError && +
+

{`Could not find weather data for city ${inputText}! :( try again`}

+
+ } +
-
-
-
) } \ No newline at end of file diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js index 0e485b9..6f7376e 100644 --- a/app/components/WeatherAPI.js +++ b/app/components/WeatherAPI.js @@ -112,6 +112,7 @@ const getWeather = async function (cityName) { return { id: uuidv4(), name: latLonData.name, + latLon: latLonData, currentWeather: currentWeatherData, fiveDayWeather: FiveDayWeatherData, } diff --git a/app/page.js b/app/page.js index 81f7707..ce71468 100644 --- a/app/page.js +++ b/app/page.js @@ -8,6 +8,11 @@ export default function Home() {
+
+
+
diff --git a/package-lock.json b/package-lock.json index 69aca92..e3d441e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "parsity_rtk_weather", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.9.0", "@reduxjs/toolkit": "^2.2.7", "bootstrap": "^5.3.3", "eslint": "8.50.0", @@ -16,8 +17,10 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.4", "react-dom": "^18.3.1", + "react-hook-form": "^7.45.4", "react-redux": "^9.1.2", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "yup": "^1.4.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -91,6 +94,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -2990,6 +3001,11 @@ "react": ">=0.14.0" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3069,6 +3085,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.45.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.4.tgz", + "integrity": "sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -3603,6 +3634,11 @@ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3614,6 +3650,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -3994,6 +4035,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 5fe4e85..bdc263a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "@reduxjs/toolkit": "^2.2.7", "bootstrap": "^5.3.3", "eslint": "8.50.0", @@ -17,7 +18,9 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.4", "react-dom": "^18.3.1", + "react-hook-form": "^7.45.4", "react-redux": "^9.1.2", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "yup": "^1.4.0" } } From b76f9a29b2edc6902c39d37f40a60de5489d42ae Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 8 Aug 2024 22:47:43 -0400 Subject: [PATCH 03/18] Added current weather population --- .vscode/settings.json | 2 +- app/components/Panel.jsx | 26 +++++++++++++++++++ app/components/SearchBar.jsx | 25 +++++------------- app/components/WeatherPanel.jsx | 25 ++++++++++++++++++ app/page.js | 46 +++------------------------------ next.config.js | 6 ++++- 6 files changed, 66 insertions(+), 64 deletions(-) create mode 100644 app/components/Panel.jsx create mode 100644 app/components/WeatherPanel.jsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 884e0d7..18edb62 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["APIKEY"] + "cSpell.words": ["APIKEY", "hookform", "openweathermap"] } diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx new file mode 100644 index 0000000..a6b1c67 --- /dev/null +++ b/app/components/Panel.jsx @@ -0,0 +1,26 @@ +import Image from "next/image"; + +export default function Panel({currentWeather}) { + const {temp, name, weather, iconCode} = currentWeather; + + return ( +
+
+
+
{temp}°
+
{name}
+
{weather}
+
+ {`${weather} +
+
+ ) +} \ No newline at end of file diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index 70c8c9e..0d847c3 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -1,5 +1,4 @@ "use client" - import { useState } from "react"; import { getWeather } from "./WeatherAPI"; import { useDispatch } from "react-redux"; @@ -8,27 +7,13 @@ import { useForm } from "react-hook-form"; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; - export default function SearchBar() { const [inputText, setInputText] = useState(""); const [searchError, setSearchError] = useState(false); const dispatch = useDispatch(); - yup.setLocale({ - mixed: { - required: 'Required Field', - }, - }); - const weatherSchema = yup - .object({ - id: yup.string().required(), - name: yup.string().required(), - latLon: yup.object().required(), - currentWeather: yup.object().required(), - fiveDayWeather: yup.array().required() - }); const formSchema = yup - .object({ - city: yup.string().required().min(5), + .object({ + city: yup.string().required().min(5), }); const { @@ -53,9 +38,12 @@ export default function SearchBar() { } else { setSearchError(false); } - + // update redux dispatch(pushLocation(weather)); + + // clear inputText + setInputText(""); } @@ -87,7 +75,6 @@ export default function SearchBar() {

{`Could not find weather data for city ${inputText}! :( try again`}

} -
diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx new file mode 100644 index 0000000..e6ae8f6 --- /dev/null +++ b/app/components/WeatherPanel.jsx @@ -0,0 +1,25 @@ +import { useSelector } from "react-redux"; +import { v4 as uuidv4 } from 'uuid'; + +import Panel from "./Panel"; + +export default function WeatherPanel() { + const weatherData = useSelector((state) => state.locations.locations); + + const panels = weatherData.map((weather) => { + return + }) + + return ( +
+
+
+ {panels} +
+
+
+ + ) +} \ No newline at end of file diff --git a/app/page.js b/app/page.js index ce71468..196fd2c 100644 --- a/app/page.js +++ b/app/page.js @@ -2,53 +2,13 @@ import Image from 'next/image' import styles from './page.module.css' import SearchBar from './components/SearchBar' +import WeatherPanel from './components/WeatherPanel' export default function Home() { return (
-
- -
-
-
-
-
-
-
-
Modal title
- -
-
-

Modal body text goes here.

-
-
- -
-
-
-
- - -
+ +
) } diff --git a/next.config.js b/next.config.js index 767719f..e1b944d 100644 --- a/next.config.js +++ b/next.config.js @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + images: { + domains: ["openweathermap.org"] + }, +}; module.exports = nextConfig From 920783161f727baaddc9700d4b8f35a47f3bddd6 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 8 Aug 2024 23:14:07 -0400 Subject: [PATCH 04/18] Fixed enter key not submitting bug --- app/components/SearchBar.jsx | 2 ++ package-lock.json | 27 ++++++++++++++++++++------- package.json | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index 0d847c3..a9f4f2d 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -25,6 +25,7 @@ export default function SearchBar() { }); const handleFormSubmit = async () => { + debugger console.log(errors) console.log("submitting ", inputText); // Get location based on city name @@ -62,6 +63,7 @@ export default function SearchBar() { placeholder="Search a city" value={inputText} onChange={({target}) => setInputText(target.value)} + onKeyDown={(e) => { e.key === 'Enter' && e.preventDefault(); }} /> diff --git a/package-lock.json b/package-lock.json index e3d441e..3cd1ab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.45.4", "react-redux": "^9.1.2", + "react-sparklines": "^1.7.0", "uuid": "^10.0.0", "yup": "^1.4.0" } @@ -907,11 +908,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1722,9 +1723,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3132,6 +3133,18 @@ } } }, + "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==", + "dependencies": { + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index bdc263a..cc6969c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.45.4", "react-redux": "^9.1.2", + "react-sparklines": "^1.7.0", "uuid": "^10.0.0", "yup": "^1.4.0" } From 668aad76cd573ee7cfcbbd69c9f2145d89b34750 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 8 Aug 2024 23:33:27 -0400 Subject: [PATCH 05/18] set up graph example --- .vscode/settings.json | 2 +- app/components/Panel.jsx | 16 +++++++++++++--- app/components/SearchBar.jsx | 4 +--- app/components/WeatherPanel.jsx | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 18edb62..38bc422 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["APIKEY", "hookform", "openweathermap"] + "cSpell.words": ["APIKEY", "hookform", "openweathermap", "Sparklines"] } diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index a6b1c67..6b7f22a 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,7 +1,11 @@ import Image from "next/image"; +import { Sparklines, SparklinesLine } from "react-sparklines"; -export default function Panel({currentWeather}) { +export default function Panel({currentWeather, fiveDayWeather}) { const {temp, name, weather, iconCode} = currentWeather; + const tempsArray = fiveDayWeather.map(day => { + return day.temp + }) return (
{name}
{weather}
- {`${weather} + /> */} +
+ + + + +
) diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index a9f4f2d..2b61d89 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -25,9 +25,7 @@ export default function SearchBar() { }); const handleFormSubmit = async () => { - debugger - console.log(errors) - console.log("submitting ", inputText); + // Get location based on city name const weather = await getWeather(inputText); console.log(weather) diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index e6ae8f6..6d034a2 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -7,7 +7,7 @@ export default function WeatherPanel() { const weatherData = useSelector((state) => state.locations.locations); const panels = weatherData.map((weather) => { - return + return }) return ( From 60d5f59404870725ea145222707ba425394f186c Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Fri, 9 Aug 2024 12:12:34 -0400 Subject: [PATCH 06/18] added fiive day weather dropdown --- app/components/FiveDayWeather.jsx | 25 +++++++++++++++++++++++++ app/components/Panel.jsx | 21 ++++++++++----------- app/components/WeatherDetails.jsx | 27 +++++++++++++++++++++++++++ app/components/WeatherPanel.jsx | 10 ++++------ 4 files changed, 66 insertions(+), 17 deletions(-) create mode 100644 app/components/FiveDayWeather.jsx create mode 100644 app/components/WeatherDetails.jsx diff --git a/app/components/FiveDayWeather.jsx b/app/components/FiveDayWeather.jsx new file mode 100644 index 0000000..e51062e --- /dev/null +++ b/app/components/FiveDayWeather.jsx @@ -0,0 +1,25 @@ +import {v4 as uuidv4} from "uuid"; +import Image from "next/image"; + +export default function FiveDayWeather({fiveDayWeather}) { + + const dayBoxes = fiveDayWeather.map((day) => { + return
+
{day.weather}
+
{day.temp}°
+ {`${day.weather} +
{day.day}
+
+ }); + + return ( +
+ {dayBoxes} +
+ ) +} \ No newline at end of file diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index 6b7f22a..7cc4097 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,36 +1,35 @@ import Image from "next/image"; import { Sparklines, SparklinesLine } from "react-sparklines"; +import WeatherDetails from "./WeatherDetails"; export default function Panel({currentWeather, fiveDayWeather}) { const {temp, name, weather, iconCode} = currentWeather; const tempsArray = fiveDayWeather.map(day => { return day.temp - }) + }); return ( -
-
+
+
{temp}°
{name}
{weather}
- {/* {`${weather} */} -
- + /> + {/*
+ - -
+
*/}
+
) } \ No newline at end of file diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx new file mode 100644 index 0000000..0648395 --- /dev/null +++ b/app/components/WeatherDetails.jsx @@ -0,0 +1,27 @@ +import FiveDayWeather from "./FiveDayWeather"; + +export default function WeatherDetails ({fiveDayWeather}) { + + return ( + <> + +
+
+ +
+
+
+
+ Weather Graphs collapse +
+
+ + ) +} \ No newline at end of file diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index 6d034a2..e6577c8 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -12,12 +12,10 @@ export default function WeatherPanel() { return (
-
-
- {panels} -
+
+ {panels}
From 1c379aea23041e04ce1d97b4bd29dbbb2a8721dc Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Fri, 9 Aug 2024 13:07:14 -0400 Subject: [PATCH 07/18] refactored props in weatherPanel --- app/components/FiveDayWeather.jsx | 18 +++++++++++------- app/components/Graphs.jsx | 9 +++++++++ app/components/Panel.jsx | 13 +++++++------ app/components/SearchBar.jsx | 13 ++++++++++--- app/components/WeatherDetails.jsx | 23 ++++++++++++++++------- app/components/WeatherPanel.jsx | 2 +- app/store/slices/locations.js | 2 +- 7 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 app/components/Graphs.jsx diff --git a/app/components/FiveDayWeather.jsx b/app/components/FiveDayWeather.jsx index e51062e..4f7b8ef 100644 --- a/app/components/FiveDayWeather.jsx +++ b/app/components/FiveDayWeather.jsx @@ -1,24 +1,28 @@ import {v4 as uuidv4} from "uuid"; import Image from "next/image"; +import { useSelector } from "react-redux"; -export default function FiveDayWeather({fiveDayWeather}) { +export default function FiveDayWeather({ id }) { + const fiveDayWeather = useSelector((state) => { + return state.locations.locations.find((day) => day.id === id).fiveDayWeather + }); const dayBoxes = fiveDayWeather.map((day) => { - return
-
{day.weather}
-
{day.temp}°
+ return
+
{day.weather}
+
{day.temp}°
{`${day.weather} -
{day.day}
+
{day.day}
}); return ( -
+
{dayBoxes}
) diff --git a/app/components/Graphs.jsx b/app/components/Graphs.jsx new file mode 100644 index 0000000..4081d69 --- /dev/null +++ b/app/components/Graphs.jsx @@ -0,0 +1,9 @@ + + +export default function Graphs ({id}) { + return ( +
+ Weather Graphs collapse +
+ ) +} \ No newline at end of file diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index 7cc4097..8e9a072 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,12 +1,13 @@ import Image from "next/image"; import { Sparklines, SparklinesLine } from "react-sparklines"; +import { useSelector } from "react-redux"; import WeatherDetails from "./WeatherDetails"; -export default function Panel({currentWeather, fiveDayWeather}) { - const {temp, name, weather, iconCode} = currentWeather; - const tempsArray = fiveDayWeather.map(day => { - return day.temp - }); + +export default function Panel({ id }) { + const {temp, name, weather, iconCode} = useSelector((state) => { + return state.locations.locations.find((day) => day.id === id).currentWeather + }) return (
@@ -29,7 +30,7 @@ export default function Panel({currentWeather, fiveDayWeather}) {
*/}
- +
) } \ No newline at end of file diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index 2b61d89..948de3f 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -11,11 +11,14 @@ export default function SearchBar() { const [inputText, setInputText] = useState(""); const [searchError, setSearchError] = useState(false); const dispatch = useDispatch(); + + // Yup schema for validation const formSchema = yup .object({ city: yup.string().required().min(5), }); + // Register form validations const { register, handleSubmit, @@ -24,13 +27,17 @@ export default function SearchBar() { resolver: yupResolver(formSchema) }); - const handleFormSubmit = async () => { + const handleOnInputChange = (target) => { + setSearchError(false); + setInputText(target.value); + }; + const handleFormSubmit = async () => { // Get location based on city name const weather = await getWeather(inputText); console.log(weather) - // Validate results + // Check if api results are empty if (Object.keys(weather).length === 0) { setSearchError(true); return; @@ -60,7 +67,7 @@ export default function SearchBar() { className={`form-control me-3 ${errors.city && "is-invalid"}`} placeholder="Search a city" value={inputText} - onChange={({target}) => setInputText(target.value)} + onChange={({target}) => handleOnInputChange(target)} onKeyDown={(e) => { e.key === 'Enter' && e.preventDefault(); }} /> diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx index 0648395..4d5ceef 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/WeatherDetails.jsx @@ -1,25 +1,34 @@ import FiveDayWeather from "./FiveDayWeather"; +import { useSelector } from "react-redux"; +import Graphs from "./Graphs"; + + +export default function WeatherDetails ({ id }) { + const weatherData = useSelector((state) => { + return state.locations.locations.find((day) => day.id === id) + }) + + if (!weatherData) return

Could not find weather data

-export default function WeatherDetails ({fiveDayWeather}) { return ( <> -
+
- +
-
+
- Weather Graphs collapse +
diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index e6577c8..c289d14 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -7,7 +7,7 @@ export default function WeatherPanel() { const weatherData = useSelector((state) => state.locations.locations); const panels = weatherData.map((weather) => { - return + return }) return ( diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js index 64257a9..98bdd9e 100644 --- a/app/store/slices/locations.js +++ b/app/store/slices/locations.js @@ -13,7 +13,7 @@ export const locationsSlice = createSlice({ }, pushLocation: (state, action) => { state.locations.push(action.payload); - } + }, } }) From bca421e94f067d5bc027d5d96148bf0c89fe7f93 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Fri, 9 Aug 2024 14:12:42 -0400 Subject: [PATCH 08/18] added graphs --- app/components/Graphs.jsx | 31 ++++++++++++++++++++++++++++--- app/components/Panel.jsx | 7 +------ app/components/WeatherAPI.js | 16 ++++++++++++---- app/components/WeatherDetails.jsx | 2 +- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/app/components/Graphs.jsx b/app/components/Graphs.jsx index 4081d69..8770eb2 100644 --- a/app/components/Graphs.jsx +++ b/app/components/Graphs.jsx @@ -1,9 +1,34 @@ - +import { Sparklines, SparklinesLine } from "react-sparklines"; +import { useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; export default function Graphs ({id}) { + const fiveDayWeather = useSelector((state) => { + return state.locations.locations.find((day) => day.id === id).fiveDayWeather + }) + + const graphData = {}; + + fiveDayWeather.forEach(day => { + graphData.temp ? graphData.temp.push(day.temp) : graphData.temp = [day.temp]; + graphData.humidity ? graphData.humidity.push(day.humidity) : graphData.humidity = [day.humidity]; + graphData.pressure ? graphData.pressure.push(day.pressure) : graphData.pressure = [day.pressure]; + }) + + const graphs = Object.keys(graphData).map((graph) => { + return ( +
+ {graph} + + + +
+ ) + }) + return ( -
- Weather Graphs collapse +
+ {graphs}
) } \ No newline at end of file diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index 8e9a072..ec48021 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,5 +1,4 @@ import Image from "next/image"; -import { Sparklines, SparklinesLine } from "react-sparklines"; import { useSelector } from "react-redux"; import WeatherDetails from "./WeatherDetails"; @@ -24,11 +23,7 @@ export default function Panel({ id }) { height={100} className="cur-weather-icon" /> - {/*
- - - -
*/} +
diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js index 6f7376e..dc2d045 100644 --- a/app/components/WeatherAPI.js +++ b/app/components/WeatherAPI.js @@ -40,6 +40,9 @@ const getFiveDayWeatherData = async function (lonLatData, units="imperial") { return formatFiveDayData(data.list); }; +const getRoundedAverage = value => Math.ceil((value / 8) * 100) / 100; + + const formatFiveDayData = function (data) { let dateSeparatedArr = []; let dateArr = [] @@ -78,6 +81,8 @@ const formatFiveDayData = function (data) { return dayArr.reduce((accumulator, itm, dayIndex) => { accumulator.temp += itm.main.temp; + accumulator.pressure += itm.main.temp; + accumulator.humidity += itm.main.humidity; // get Day of week, weather icon, and weather condition if (dayIndex === 0) { @@ -91,12 +96,15 @@ const formatFiveDayData = function (data) { // if item is last item of day calc avg temp; if (dayIndex === 7) { - const avgTemp = accumulator.temp / 8; - const roundedTemp = Math.ceil(avgTemp * 100) / 100; - accumulator.temp = roundedTemp; + // const avgTemp = accumulator.temp / 8; + + // const roundedTemp = Math.ceil(avgTemp * 100) / 100; + accumulator.temp = getRoundedAverage(accumulator.temp); + accumulator.pressure = getRoundedAverage(accumulator.pressure); + accumulator.humidity = getRoundedAverage(accumulator.humidity); }; return accumulator; - }, {"temp": 0, "weather": "", "iconCode": "", "day":""}) + }, {"temp": 0, "pressure": 0, "humidity": 0, "weather": "", "iconCode": "", "day":""}) }) return dateSeparatedArr; }; diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx index 4d5ceef..445c751 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/WeatherDetails.jsx @@ -18,7 +18,7 @@ export default function WeatherDetails ({ id }) { Five Day Forecast
From a0efc915e58a2151764e3784cd074af0c8f7d918 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Fri, 9 Aug 2024 14:22:10 -0400 Subject: [PATCH 09/18] fixed graph data --- app/components/WeatherAPI.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js index dc2d045..b973029 100644 --- a/app/components/WeatherAPI.js +++ b/app/components/WeatherAPI.js @@ -78,10 +78,9 @@ const formatFiveDayData = function (data) { * to: dateSeparatedArray = [{day1Obj}, {day2Obj}, ...] */ dateSeparatedArr = dateSeparatedArr.map((dayArr) => { - return dayArr.reduce((accumulator, itm, dayIndex) => { accumulator.temp += itm.main.temp; - accumulator.pressure += itm.main.temp; + accumulator.pressure += itm.main.pressure; accumulator.humidity += itm.main.humidity; // get Day of week, weather icon, and weather condition From 5b365d24e69560571ec4d57b5614a57e928200bb Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sat, 10 Aug 2024 12:45:47 -0400 Subject: [PATCH 10/18] added more validation --- .vscode/settings.json | 8 +++- app/components/AppNavbar.jsx | 37 +++++++++--------- app/components/CurrentWeatherDetails.jsx | 27 +++++++++++++ app/components/ErrorMessage.jsx | 8 ++++ app/components/Graphs.jsx | 27 ++++++++----- app/components/Panel.jsx | 22 ++--------- app/components/SearchBar.jsx | 49 +++++++++++++++--------- app/components/WeatherAPI.js | 45 +++++++++------------- app/components/WeatherDetails.jsx | 16 ++++---- app/components/WeatherPanel.jsx | 17 ++++++-- app/layout.js | 18 ++++----- app/page.js | 8 ++-- app/store/slices/locations.js | 1 - 13 files changed, 166 insertions(+), 117 deletions(-) create mode 100644 app/components/CurrentWeatherDetails.jsx create mode 100644 app/components/ErrorMessage.jsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 38bc422..053e305 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "cSpell.words": ["APIKEY", "hookform", "openweathermap", "Sparklines"] + "cSpell.words": [ + "accum", + "APIKEY", + "hookform", + "openweathermap", + "Sparklines" + ] } diff --git a/app/components/AppNavbar.jsx b/app/components/AppNavbar.jsx index 40effdd..fd52304 100644 --- a/app/components/AppNavbar.jsx +++ b/app/components/AppNavbar.jsx @@ -1,34 +1,33 @@ "use client" -import { useSelector, useDispatch } from "react-redux"; +import { useDispatch } from "react-redux"; import { setCurrentLocation } from "../store/slices/locations"; - +import { getWeather } from "./WeatherAPI"; export default function AppNavbar() { + const dispatch = useDispatch(); - const currentLocation = useSelector((state) => {state.locations.currentLocation}) + const onSuccess = async (data) => { + try { + if (!data) return + const { latitude, longitude } = data.coords; + const weatherData = await getWeather("", {latitude, longitude}) + dispatch(setCurrentLocation(weatherData)); + } catch (e) { + console.log(e); + } + }; - const dispatch = useDispatch(); + const onError = (error) => { + console.log(error) + }; - const handleSetLocation = () => { - console.log("Handling Location"); + const handleSetLocation = (navigator, onSuccess, onError) => { const geolocation = navigator.geolocation; if (!geolocation) return alert("Geolocation is not supported. Sorry!"); - - const onSuccess = (data) => { - console.log(data); - const { latitude, longitude } = data.coords; - console.log({latitude, longitude}) - dispatch(setCurrentLocation({latitude, longitude})); - }; - const onError = (error) => { - console.log(error) - }; geolocation.getCurrentPosition(onSuccess, onError, {enableHighAccuracy:true}); - }; - return (
{errors.city && -
-

{errors.city.message}

-
+ } {searchError && -
-

{`Could not find weather data for city ${inputText}! :( try again`}

-
+ }
diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js index b973029..1c2668f 100644 --- a/app/components/WeatherAPI.js +++ b/app/components/WeatherAPI.js @@ -31,7 +31,7 @@ const getCurrentWeatherData = async function (lonLatData, unit="imperial") { }; const getFiveDayWeatherData = async function (lonLatData, units="imperial") { - const {lon, lat, name,} = lonLatData; + const {lon, lat, name = ""} = lonLatData; const url = `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lon}&units=${units}&appid=${APIKEY}`; const data = await fetchData(url); @@ -45,26 +45,13 @@ const getRoundedAverage = value => Math.ceil((value / 8) * 100) / 100; const formatFiveDayData = function (data) { let dateSeparatedArr = []; - let dateArr = [] - /** - * Formatting logic: - * Loop through data array (40 items where every 8 items is 1 day) while keep track of number items traversed - * We append each item to a temp array (dateArr) - * Everytime we've traversed 8 items (1 day) we push the temp array to the output array (dateSeparatedArr) - */ - data.forEach((itm, index) => { - const itmCt = index + 1; - // if itemCt is devisable by 8, it means it is the last itm for that day - if ((itmCt % 8) === 0){ - dateArr.push(itm); - dateSeparatedArr.push(dateArr); - dateArr = []; - return - } - dateArr.push(itm); - return - }); + // Slice every chunks of 8 items from data array and append to dateSeperatedArr + for (let i = 0; i < data.length; i += 8) { + const chunk = data.slice(i, i + 8); + dateSeparatedArr.push(chunk); + } + /** * Formatting logic: @@ -108,17 +95,23 @@ const formatFiveDayData = function (data) { return dateSeparatedArr; }; -const getWeather = async function (cityName) { - const latLonData = await getLatLonData(cityName); - if (Object.keys(latLonData).length === 0) { - return {} +const getWeather = async function (cityName = "", latLon = null) { + let latLonData; + if (cityName) { + latLonData = await getLatLonData(cityName); + + if (Object.keys(latLonData).length === 0) { + return {} + } + } else { + latLonData = {lat: latLon.latitude, lon: latLon.longitude} } const currentWeatherData = await getCurrentWeatherData(latLonData); const FiveDayWeatherData = await getFiveDayWeatherData(latLonData); - + return { id: uuidv4(), - name: latLonData.name, + name: currentWeatherData.name, latLon: latLonData, currentWeather: currentWeatherData, fiveDayWeather: FiveDayWeatherData, diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx index 445c751..26cab94 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/WeatherDetails.jsx @@ -21,14 +21,16 @@ export default function WeatherDetails ({ id }) { Five Day Graphs
-
-
- +
+
+
+ +
-
-
-
- +
+
+ +
diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index c289d14..84f236a 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -1,20 +1,29 @@ +import { useEffect } from "react"; import { useSelector } from "react-redux"; import { v4 as uuidv4 } from 'uuid'; - import Panel from "./Panel"; +import { useDispatch } from "react-redux"; +import { pushLocation } from "../store/slices/locations"; export default function WeatherPanel() { const weatherData = useSelector((state) => state.locations.locations); + const currentLocation = useSelector((state) => state.locations.currentLocation); + const dispatch = useDispatch(); + + // Create panels from state const panels = weatherData.map((weather) => { return }) + + useEffect(() => { + currentLocation && dispatch(pushLocation(currentLocation)); + }, [currentLocation, dispatch]) + return (
-
+
{panels}
diff --git a/app/layout.js b/app/layout.js index 01b1340..946ff70 100644 --- a/app/layout.js +++ b/app/layout.js @@ -15,15 +15,15 @@ export const metadata = { export default function RootLayout({ children }) { return ( <> - - - - - {children} - - - - + + + + + {children} + + + + ) } diff --git a/app/page.js b/app/page.js index 196fd2c..da9d338 100644 --- a/app/page.js +++ b/app/page.js @@ -1,8 +1,8 @@ "use client" -import Image from 'next/image' -import styles from './page.module.css' -import SearchBar from './components/SearchBar' -import WeatherPanel from './components/WeatherPanel' +import Image from 'next/image'; +import styles from './page.module.css'; +import SearchBar from './components/SearchBar'; +import WeatherPanel from './components/WeatherPanel'; export default function Home() { return ( diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js index 98bdd9e..565e7f4 100644 --- a/app/store/slices/locations.js +++ b/app/store/slices/locations.js @@ -5,7 +5,6 @@ export const locationsSlice = createSlice({ initialState: { currentLocation: null, locations: [], - activeLocation: null, }, reducers: { setCurrentLocation: (state, action) => { From 9082a011114eff100ae93569ec004842ad5d4eb8 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Sat, 10 Aug 2024 14:36:30 -0400 Subject: [PATCH 11/18] seperated form from search bar --- app/components/SearchBar.jsx | 64 ++++---------------------------- app/components/SearchBarForm.jsx | 55 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 app/components/SearchBarForm.jsx diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index 9b78258..5b93990 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -3,10 +3,8 @@ import { useState } from "react"; import { getWeather } from "./WeatherAPI"; import { useDispatch } from "react-redux"; import { pushLocation } from "../store/slices/locations"; -import { useForm } from "react-hook-form"; -import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; -import Error from "./ErrorMessage"; +import SearchBarForm from "./SearchBarForm"; const errorList = { apiFail: "There was error with your api", @@ -14,16 +12,8 @@ const errorList = { } export default function SearchBar() { - const [inputText, setInputText] = useState(""); - const [searchError, setSearchError] = useState(false); + const [searchErrors, setSearchErrors] = useState([]); const dispatch = useDispatch(); - const [ errorMessage, setErrorMessage ] = useState(""); - - // Yup schema for validation - const formSchema = yup - .object({ - city: yup.string().required().min(3), - }); const weatherSchema = yup .object({ @@ -32,40 +22,24 @@ export default function SearchBar() { latLon: yup.object().required(), currentWeather: yup.object().required(), fiveDayWeather: yup.array().required() - }) + }) - // Register form validations - const { - register, - handleSubmit, - formState: { errors } - } = useForm({ - resolver: yupResolver(formSchema) - }); + const handleFormSubmit = async (inputText) => { - // Change state value and reset search errors - const handleOnInputChange = (target) => { - setSearchError(false); - setInputText(target.value); - }; + setSearchErrors([]); - const handleFormSubmit = async () => { try { // Get location based on city name const weather = await getWeather(inputText); - if (!weather) setErrorMessage(errorList.apiFail); + // Validate Api data await weatherSchema.validate(weather); // update redux dispatch(pushLocation(weather)); - // clear inputText - setInputText(""); } catch (e) { - console.log(e.errors); - setSearchError(true); - setErrorMessage(errorList.type); + setSearchErrors(prev => [{message: `Could not find the city ${inputText}.`,}, ...prev]); } } @@ -74,29 +48,7 @@ export default function SearchBar() {
-
-
-
- handleOnInputChange(target)} - onKeyDown={(e) => { e.key === 'Enter' && e.preventDefault(); }} - /> - -
- {errors.city && - - } - {searchError && - - } -
-
+
diff --git a/app/components/SearchBarForm.jsx b/app/components/SearchBarForm.jsx new file mode 100644 index 0000000..ec0bc9e --- /dev/null +++ b/app/components/SearchBarForm.jsx @@ -0,0 +1,55 @@ +import Error from "./ErrorMessage"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import {v4 as uuidv4} from 'uuid'; + + +export default function SearchBarForm({handleFormSubmit, apiErrors = []}) { + const [inputText, setInputText] = useState(""); + const formSchema = yup + .object({ + city: yup.string().required().min(3), + }); + + const { + register, + handleSubmit, + formState: { errors } + } = useForm({ + resolver: yupResolver(formSchema) + }); + + const handleOnInputChange = (target) => setInputText(target.value); + + const onSubmit = () => { + handleFormSubmit(inputText); + setInputText("") + } + return ( +
+
+
+ handleOnInputChange(target)} + onKeyDown={(e) => { e.key === 'Enter' && e.preventDefault(); }} + /> + +
+ {errors.city && + + } + {apiErrors.map((error) => { + return error.message && + })} +
+
+ ) +} \ No newline at end of file From a92fc4f01d82e24a22fa7897a8ac474578dfbed9 Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Sat, 10 Aug 2024 17:38:43 -0400 Subject: [PATCH 12/18] added default location. Need to fix double render bug --- app/components/AppNavbar.jsx | 7 +++-- app/components/Modal.jsx | 50 +++++++++++++++++++++++++++++++ app/components/Panel.jsx | 2 -- app/components/WeatherAPI.js | 8 +++-- app/components/WeatherDetails.jsx | 17 +++++++++-- app/page.js | 28 +++++++++++++++-- app/store/slices/locations.js | 6 +++- next.config.js | 5 +++- 8 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 app/components/Modal.jsx diff --git a/app/components/AppNavbar.jsx b/app/components/AppNavbar.jsx index fd52304..125ffbd 100644 --- a/app/components/AppNavbar.jsx +++ b/app/components/AppNavbar.jsx @@ -1,16 +1,17 @@ "use client" -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { setCurrentLocation } from "../store/slices/locations"; import { getWeather } from "./WeatherAPI"; export default function AppNavbar() { const dispatch = useDispatch(); + const defaultLocation = useSelector(state => state.locations.defaultLocation); const onSuccess = async (data) => { try { if (!data) return const { latitude, longitude } = data.coords; - const weatherData = await getWeather("", {latitude, longitude}) + const weatherData = await getWeather("", {lat : latitude, lon: longitude}) dispatch(setCurrentLocation(weatherData)); } catch (e) { console.log(e); @@ -35,7 +36,7 @@ export default function AppNavbar() { RTK Weather
  • - Default location: Not set + Default location: {defaultLocation.name? defaultLocation.name : "Not Set"}
setShow(false); + const handleShow = (e) => { + e.preventDefault() + setShow(true) + }; + + const handleSubmit = (action) => { + console.log("Handling set default") + action(); + handleClose(); + } + + return ( + <> + {handleShow(e)}}> + {text} + + + {/* Modal */} +
+
+
+
+
{title}
+
+
+

{body}

+
+
+ + +
+
+
+
+ + {/* Background overlay for modal */} + {show &&
} + + ); +}; diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index 8205a76..aa11996 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,5 +1,3 @@ -import Image from "next/image"; -import { useSelector } from "react-redux"; import WeatherDetails from "./WeatherDetails"; import CurrentWeatherDetails from "./CurrentWeatherDetails"; diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js index 1c2668f..d404cad 100644 --- a/app/components/WeatherAPI.js +++ b/app/components/WeatherAPI.js @@ -95,16 +95,18 @@ const formatFiveDayData = function (data) { return dateSeparatedArr; }; -const getWeather = async function (cityName = "", latLon = null) { +const getWeather = async function ( cityName, latLon = null) { + console.log("LATLON : ", latLon) + console.log(cityName); let latLonData; - if (cityName) { + if (cityName !== "") { latLonData = await getLatLonData(cityName); if (Object.keys(latLonData).length === 0) { return {} } } else { - latLonData = {lat: latLon.latitude, lon: latLon.longitude} + latLonData = {lat: latLon.lat, lon: latLon.lon} } const currentWeatherData = await getCurrentWeatherData(latLonData); const FiveDayWeatherData = await getFiveDayWeatherData(latLonData); diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx index 26cab94..1a3c6dc 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/WeatherDetails.jsx @@ -1,12 +1,13 @@ import FiveDayWeather from "./FiveDayWeather"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import Graphs from "./Graphs"; +import Modal from "./Modal"; export default function WeatherDetails ({ id }) { const weatherData = useSelector((state) => { return state.locations.locations.find((day) => day.id === id) - }) + }); if (!weatherData) return

Could not find weather data

@@ -33,6 +34,18 @@ export default function WeatherDetails ({ id }) {
+
+ { + localStorage.setItem("RTKWEATHER_DEFAULTLOCATION", JSON.stringify(weatherData.latLon)) + }} + text={"set Default"} + title={"Set as default location?"} + body={`Would you like to set "${weatherData.name}" location as your default location?`} + closeText="No" + submitText="Yes please!" + /> +
) } \ No newline at end of file diff --git a/app/page.js b/app/page.js index da9d338..3390e44 100644 --- a/app/page.js +++ b/app/page.js @@ -1,10 +1,34 @@ "use client" -import Image from 'next/image'; -import styles from './page.module.css'; import SearchBar from './components/SearchBar'; import WeatherPanel from './components/WeatherPanel'; +import { useDispatch } from 'react-redux'; +import { setDefaultLocation, pushLocation } from './store/slices/locations'; +import { getWeather } from './components/WeatherAPI'; +import { useEffect } from 'react'; + export default function Home() { + const dispatch = useDispatch(); + + const handleSetDefault = async (location) => { + try { + debugger + const weatherData = await getWeather("", location); + dispatch(setDefaultLocation(weatherData)); + dispatch(pushLocation(weatherData)); + } catch (e) { + console.log(e) + } + }; + + useEffect(() => { + const defaultLocation = JSON.parse(localStorage.getItem("RTKWEATHER_DEFAULTLOCATION")); + + if (defaultLocation) { + handleSetDefault(defaultLocation); + }; + }) + return (
diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js index 565e7f4..619b85d 100644 --- a/app/store/slices/locations.js +++ b/app/store/slices/locations.js @@ -5,6 +5,7 @@ export const locationsSlice = createSlice({ initialState: { currentLocation: null, locations: [], + defaultLocation: null, }, reducers: { setCurrentLocation: (state, action) => { @@ -13,9 +14,12 @@ export const locationsSlice = createSlice({ pushLocation: (state, action) => { state.locations.push(action.payload); }, + setDefaultLocation: (state, action) => { + state.defaultLocation = action.payload; + } } }) -export const { setCurrentLocation, pushLocation } = locationsSlice.actions; +export const { setCurrentLocation, pushLocation, setDefaultLocation } = locationsSlice.actions; export default locationsSlice.reducer; \ No newline at end of file diff --git a/next.config.js b/next.config.js index e1b944d..76c5199 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,10 @@ /** @type {import('next').NextConfig} */ const nextConfig = { images: { - domains: ["openweathermap.org"] + remotePatterns: [{ + protocol: "https", + hostname:"openweathermap.org" + }] }, }; From ab9bbf36429a242852f315f6adebe90a5d43fa7a Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Sat, 10 Aug 2024 20:39:10 -0400 Subject: [PATCH 13/18] re-factored default location --- app/components/AppNavbar.jsx | 2 +- app/components/WeatherAPI.js | 4 ---- app/components/WeatherDetails.jsx | 5 ++++- app/components/WeatherPanel.jsx | 19 ++++++++++++------- app/page.js | 25 ------------------------- app/store/slices/locations.js | 6 ++++++ 6 files changed, 23 insertions(+), 38 deletions(-) diff --git a/app/components/AppNavbar.jsx b/app/components/AppNavbar.jsx index 125ffbd..a8ae6c6 100644 --- a/app/components/AppNavbar.jsx +++ b/app/components/AppNavbar.jsx @@ -36,7 +36,7 @@ export default function AppNavbar() { RTK Weather
  • - Default location: {defaultLocation.name? defaultLocation.name : "Not Set"} + Default location: {defaultLocation ? defaultLocation.name : "Not Set"}
{ return state.locations.locations.find((day) => day.id === id) }); @@ -37,7 +39,8 @@ export default function WeatherDetails ({ id }) {
{ - localStorage.setItem("RTKWEATHER_DEFAULTLOCATION", JSON.stringify(weatherData.latLon)) + localStorage.setItem("RTKWEATHER_DEFAULTLOCATION", JSON.stringify(weatherData)) + dispatch(setDefaultLocation(weatherData)) }} text={"set Default"} title={"Set as default location?"} diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index 84f236a..0204de3 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -1,23 +1,28 @@ import { useEffect } from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { v4 as uuidv4 } from 'uuid'; import Panel from "./Panel"; -import { useDispatch } from "react-redux"; -import { pushLocation } from "../store/slices/locations"; +import { pushLocation, setDefaultLocation } from "../store/slices/locations"; export default function WeatherPanel() { const weatherData = useSelector((state) => state.locations.locations); - const currentLocation = useSelector((state) => state.locations.currentLocation); + const defaultLocation = useSelector((state) => state.locations.defaultLocation); + const dispatch = useDispatch(); // Create panels from state - const panels = weatherData.map((weather) => { - return - }) + const panels = weatherData.map((weather) => { + return + }) useEffect(() => { currentLocation && dispatch(pushLocation(currentLocation)); + const storedLocation = JSON.parse(localStorage.getItem("RTKWEATHER_DEFAULTLOCATION")); + if (storedLocation) { + dispatch(setDefaultLocation(storedLocation)); + dispatch(pushLocation(storedLocation)); + } }, [currentLocation, dispatch]) diff --git a/app/page.js b/app/page.js index 3390e44..9c2d25c 100644 --- a/app/page.js +++ b/app/page.js @@ -1,34 +1,9 @@ "use client" import SearchBar from './components/SearchBar'; import WeatherPanel from './components/WeatherPanel'; -import { useDispatch } from 'react-redux'; -import { setDefaultLocation, pushLocation } from './store/slices/locations'; -import { getWeather } from './components/WeatherAPI'; -import { useEffect } from 'react'; export default function Home() { - const dispatch = useDispatch(); - - const handleSetDefault = async (location) => { - try { - debugger - const weatherData = await getWeather("", location); - dispatch(setDefaultLocation(weatherData)); - dispatch(pushLocation(weatherData)); - } catch (e) { - console.log(e) - } - }; - - useEffect(() => { - const defaultLocation = JSON.parse(localStorage.getItem("RTKWEATHER_DEFAULTLOCATION")); - - if (defaultLocation) { - handleSetDefault(defaultLocation); - }; - }) - return (
diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js index 619b85d..78c74df 100644 --- a/app/store/slices/locations.js +++ b/app/store/slices/locations.js @@ -12,6 +12,12 @@ export const locationsSlice = createSlice({ state.currentLocation = action.payload; }, pushLocation: (state, action) => { + + const existInState = state.locations.find((itm) => {itm.id === action.payload.id}); + debugger + if (existInState) { + console.log('It exists') + } state.locations.push(action.payload); }, setDefaultLocation: (state, action) => { From 2f59e1defdba9c0873b82969ecca448fe93a04f2 Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Sun, 11 Aug 2024 15:25:52 -0400 Subject: [PATCH 14/18] Fixed double load of default weather data --- app/components/WeatherPanel.jsx | 1 - app/store/slices/locations.js | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index 0204de3..6ec72ec 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -32,6 +32,5 @@ export default function WeatherPanel() { {panels}
- ) } \ No newline at end of file diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js index 78c74df..02267a8 100644 --- a/app/store/slices/locations.js +++ b/app/store/slices/locations.js @@ -12,12 +12,10 @@ export const locationsSlice = createSlice({ state.currentLocation = action.payload; }, pushLocation: (state, action) => { - - const existInState = state.locations.find((itm) => {itm.id === action.payload.id}); - debugger + const existInState = state.locations.find(itm => itm.id === action.payload.id); if (existInState) { - console.log('It exists') - } + return + } state.locations.push(action.payload); }, setDefaultLocation: (state, action) => { From c208d7d74550a0a79b6db261aa7adf5e158bfa1f Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Sun, 11 Aug 2024 20:20:52 -0400 Subject: [PATCH 15/18] refactored code --- app/components/CurrentWeatherDetails.jsx | 21 ++++----- app/components/FiveDayWeather.jsx | 5 +-- app/components/Graphs.jsx | 7 +-- app/components/Modal.jsx | 3 +- app/components/Panel.jsx | 8 ++-- app/components/SearchBar.jsx | 50 +++++++++------------ app/components/SearchBarForm.jsx | 35 ++++----------- app/components/WeatherDetails.jsx | 54 ++++++++++++----------- app/components/WeatherPanel.jsx | 25 +++++------ app/components/useReturnDataValidation.js | 13 ++++++ app/components/useSearchBarValidation.js | 22 +++++++++ app/globals.css | 4 ++ app/page.js | 1 - app/store/slices/locations.js | 10 ++--- 14 files changed, 130 insertions(+), 128 deletions(-) create mode 100644 app/components/useReturnDataValidation.js create mode 100644 app/components/useSearchBarValidation.js diff --git a/app/components/CurrentWeatherDetails.jsx b/app/components/CurrentWeatherDetails.jsx index 398b867..e68d9b1 100644 --- a/app/components/CurrentWeatherDetails.jsx +++ b/app/components/CurrentWeatherDetails.jsx @@ -1,25 +1,26 @@ import Image from "next/image"; import { useSelector } from "react-redux"; -export default function CurrentWeatherDetails({id}) { - const {temp, name, weather, iconCode} = useSelector((state) => { - return state.locations.locations.find((day) => day.id === id).currentWeather - }); +export default function CurrentWeatherDetails({id, currentWeatherDetails}) { + // const currentWeatherDetails = useSelector((state) => { + // return state.locations.locations.find((day) => day.id === id).currentWeather + // }); - if (!name) return

Could not load location!

+ if (!currentWeatherDetails.name) return

Could not load location!

return (
-
{temp}°
-
{name}
-
{weather}
+
{currentWeatherDetails.temp}°
+
{currentWeatherDetails.name}
+
{currentWeatherDetails.weather}
{`${weather}
diff --git a/app/components/FiveDayWeather.jsx b/app/components/FiveDayWeather.jsx index 4f7b8ef..000df5e 100644 --- a/app/components/FiveDayWeather.jsx +++ b/app/components/FiveDayWeather.jsx @@ -2,10 +2,7 @@ import {v4 as uuidv4} from "uuid"; import Image from "next/image"; import { useSelector } from "react-redux"; -export default function FiveDayWeather({ id }) { - const fiveDayWeather = useSelector((state) => { - return state.locations.locations.find((day) => day.id === id).fiveDayWeather - }); +export default function FiveDayWeather({ fiveDayWeather }) { const dayBoxes = fiveDayWeather.map((day) => { return
diff --git a/app/components/Graphs.jsx b/app/components/Graphs.jsx index fab790d..c94e167 100644 --- a/app/components/Graphs.jsx +++ b/app/components/Graphs.jsx @@ -2,12 +2,7 @@ import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-spark import { useSelector } from "react-redux"; import { v4 as uuidv4 } from "uuid"; -export default function Graphs ({id}) { - - // returns and an array of objects representing a weather for one day - const fiveDayWeather = useSelector((state) => { - return state.locations.locations.find((day) => day.id === id).fiveDayWeather - }) +export default function Graphs ({ fiveDayWeather }) { // Reduce five day weather to only an object of temp, humidity, pressure arrays. const graphData = fiveDayWeather.reduce((accum, day) => { diff --git a/app/components/Modal.jsx b/app/components/Modal.jsx index 41d20e7..f75d3ed 100644 --- a/app/components/Modal.jsx +++ b/app/components/Modal.jsx @@ -2,7 +2,6 @@ import { useState } from "react"; export default function Modal ({action, text, title = "Modal", body= "", closeText = "Close", submitText="Save Changes"}) { const [show, setShow] = useState(false); - const handleClose = () => setShow(false); const handleShow = (e) => { e.preventDefault() @@ -17,7 +16,7 @@ export default function Modal ({action, text, title = "Modal", body= "", closeTe return ( <> - {handleShow(e)}}> + {handleShow(e)}}> {text} diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index aa11996..31cbb42 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,13 +1,13 @@ import WeatherDetails from "./WeatherDetails"; import CurrentWeatherDetails from "./CurrentWeatherDetails"; +import { useSelector } from "react-redux"; +export default function Panel({ weatherDetails}) { -export default function Panel({ id }) { - return (
- - + +
) } \ No newline at end of file diff --git a/app/components/SearchBar.jsx b/app/components/SearchBar.jsx index 5b93990..4901e2c 100644 --- a/app/components/SearchBar.jsx +++ b/app/components/SearchBar.jsx @@ -3,52 +3,44 @@ import { useState } from "react"; import { getWeather } from "./WeatherAPI"; import { useDispatch } from "react-redux"; import { pushLocation } from "../store/slices/locations"; -import * as yup from 'yup'; import SearchBarForm from "./SearchBarForm"; - -const errorList = { - apiFail: "There was error with your api", - type: "Wrong data type" -} +import useReturnDataValidation from "./useReturnDataValidation" export default function SearchBar() { - const [searchErrors, setSearchErrors] = useState([]); + const [searchErrors, setSearchErrors] = useState(null); const dispatch = useDispatch(); - const weatherSchema = yup - .object({ - id: yup.string().required(), - name: yup.string().required(), - latLon: yup.object().required(), - currentWeather: yup.object().required(), - fiveDayWeather: yup.array().required() - }) + const weatherSchema = useReturnDataValidation() const handleFormSubmit = async (inputText) => { - - setSearchErrors([]); - - try { - // Get location based on city name - const weather = await getWeather(inputText); - - // Validate Api data - await weatherSchema.validate(weather); + // Reset errors + setSearchErrors(null); - // update redux - dispatch(pushLocation(weather)); + try { + // Get location based on city name + const weather = await getWeather(inputText); + + // Validate Api data using yup + await weatherSchema.validate(weather); + + // update redux + dispatch(pushLocation(weather)); } catch (e) { - setSearchErrors(prev => [{message: `Could not find the city ${inputText}.`,}, ...prev]); + // On validation fail set error message + setSearchErrors({message: `Could not find the city ${inputText}.`}); } - } + }; return (
- +
diff --git a/app/components/SearchBarForm.jsx b/app/components/SearchBarForm.jsx index ec0bc9e..bab6200 100644 --- a/app/components/SearchBarForm.jsx +++ b/app/components/SearchBarForm.jsx @@ -1,32 +1,19 @@ -import Error from "./ErrorMessage"; import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { yupResolver } from '@hookform/resolvers/yup'; -import * as yup from 'yup'; +import useSearchBarValidation from "./useSearchBarValidation"; import {v4 as uuidv4} from 'uuid'; +import Error from "./ErrorMessage"; - -export default function SearchBarForm({handleFormSubmit, apiErrors = []}) { +export default function SearchBarForm({handleFormSubmit, returnDataError = null}) { const [inputText, setInputText] = useState(""); - const formSchema = yup - .object({ - city: yup.string().required().min(3), - }); - - const { - register, - handleSubmit, - formState: { errors } - } = useForm({ - resolver: yupResolver(formSchema) - }); + const {register, handleSubmit, errors} = useSearchBarValidation(); const handleOnInputChange = (target) => setInputText(target.value); const onSubmit = () => { handleFormSubmit(inputText); - setInputText("") - } + setInputText(""); + }; + return (
@@ -43,12 +30,8 @@ export default function SearchBarForm({handleFormSubmit, apiErrors = []}) { />
- {errors.city && - - } - {apiErrors.map((error) => { - return error.message && - })} + { errors.city && } + { returnDataError && }
) diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx index fe4015c..b40fd35 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/WeatherDetails.jsx @@ -5,49 +5,53 @@ import Graphs from "./Graphs"; import Modal from "./Modal"; -export default function WeatherDetails ({ id }) { +export default function WeatherDetails ({ weatherDetails }) { const dispatch = useDispatch(); - const weatherData = useSelector((state) => { - return state.locations.locations.find((day) => day.id === id) - }); - - if (!weatherData) return

Could not find weather data

- return ( <> -
-
+
+
- +
- +
-
- { - localStorage.setItem("RTKWEATHER_DEFAULTLOCATION", JSON.stringify(weatherData)) - dispatch(setDefaultLocation(weatherData)) - }} - text={"set Default"} - title={"Set as default location?"} - body={`Would you like to set "${weatherData.name}" location as your default location?`} - closeText="No" - submitText="Yes please!" - /> +
+ + + + + + + + + + + { + localStorage.setItem("RTKWEATHER_DEFAULTLOCATION", JSON.stringify(weatherDetails)) + dispatch(setDefaultLocation(weatherDetails)) + }} + text={"set Default"} + title={"Set as default location?"} + body={`Would you like to set "${weatherDetails.name}" location as your default location?`} + closeText="No" + submitText="Yes please!" + />
) diff --git a/app/components/WeatherPanel.jsx b/app/components/WeatherPanel.jsx index 6ec72ec..5b6618b 100644 --- a/app/components/WeatherPanel.jsx +++ b/app/components/WeatherPanel.jsx @@ -5,31 +5,26 @@ import Panel from "./Panel"; import { pushLocation, setDefaultLocation } from "../store/slices/locations"; export default function WeatherPanel() { - const weatherData = useSelector((state) => state.locations.locations); - const currentLocation = useSelector((state) => state.locations.currentLocation); - const defaultLocation = useSelector((state) => state.locations.defaultLocation); - const dispatch = useDispatch(); - - // Create panels from state - const panels = weatherData.map((weather) => { - return - }) + const locations = useSelector((state) => state.locations.locations); + const currentLocation = useSelector((state) => state.locations.currentLocation); useEffect(() => { + // Check for currentLocation and defaultLocation and dispatch to redux currentLocation && dispatch(pushLocation(currentLocation)); - const storedLocation = JSON.parse(localStorage.getItem("RTKWEATHER_DEFAULTLOCATION")); - if (storedLocation) { - dispatch(setDefaultLocation(storedLocation)); - dispatch(pushLocation(storedLocation)); + const defaultLocation = JSON.parse(localStorage.getItem("RTKWEATHER_DEFAULTLOCATION")); + if (defaultLocation) { + dispatch(setDefaultLocation(defaultLocation)); + dispatch(pushLocation(defaultLocation)); } }, [currentLocation, dispatch]) - return (
- {panels} + {locations.map(location => + + )}
) diff --git a/app/components/useReturnDataValidation.js b/app/components/useReturnDataValidation.js new file mode 100644 index 0000000..d9b8215 --- /dev/null +++ b/app/components/useReturnDataValidation.js @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export default function useReturnDataValidation() { + const weatherSchema = yup.object({ + id: yup.string().required(), + name: yup.string().required(), + latLon: yup.object().required(), + currentWeather: yup.object().required(), + fiveDayWeather: yup.array().required() + }); + + return weatherSchema +} \ No newline at end of file diff --git a/app/components/useSearchBarValidation.js b/app/components/useSearchBarValidation.js new file mode 100644 index 0000000..b3c5a26 --- /dev/null +++ b/app/components/useSearchBarValidation.js @@ -0,0 +1,22 @@ + +import { useForm } from "react-hook-form"; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + + +export default function useFormValidation() { + const formSchema = yup + .object({ + city: yup.string().required().min(3), + }); + + const { + register, + handleSubmit, + formState: { errors } + } = useForm({ + resolver: yupResolver(formSchema) + }); + + return {register, handleSubmit, errors } +} diff --git a/app/globals.css b/app/globals.css index e3ba175..00aef56 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,7 @@ +.no-decorations { + text-decoration: none; +} + * { box-sizing: border-box; padding: 0; diff --git a/app/page.js b/app/page.js index 9c2d25c..46d5cfc 100644 --- a/app/page.js +++ b/app/page.js @@ -2,7 +2,6 @@ import SearchBar from './components/SearchBar'; import WeatherPanel from './components/WeatherPanel'; - export default function Home() { return (
diff --git a/app/store/slices/locations.js b/app/store/slices/locations.js index 02267a8..6bbf610 100644 --- a/app/store/slices/locations.js +++ b/app/store/slices/locations.js @@ -1,21 +1,19 @@ -import { createSlice, current } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; export const locationsSlice = createSlice({ name: "locations", initialState: { currentLocation: null, - locations: [], defaultLocation: null, + locations: [], }, reducers: { setCurrentLocation: (state, action) => { state.currentLocation = action.payload; }, pushLocation: (state, action) => { - const existInState = state.locations.find(itm => itm.id === action.payload.id); - if (existInState) { - return - } + const locationInState = state.locations.find(itm => itm.id === action.payload.id); + if (locationInState) return; state.locations.push(action.payload); }, setDefaultLocation: (state, action) => { From 19fd7ac046305b7d0881cddd721df68c68baad05 Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Sun, 11 Aug 2024 20:26:54 -0400 Subject: [PATCH 16/18] debugged the refactor --- app/components/FiveDayWeather.jsx | 3 +-- app/components/Graphs.jsx | 1 - app/components/WeatherDetails.jsx | 6 +++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/components/FiveDayWeather.jsx b/app/components/FiveDayWeather.jsx index 000df5e..d9c1799 100644 --- a/app/components/FiveDayWeather.jsx +++ b/app/components/FiveDayWeather.jsx @@ -1,9 +1,8 @@ import {v4 as uuidv4} from "uuid"; import Image from "next/image"; -import { useSelector } from "react-redux"; export default function FiveDayWeather({ fiveDayWeather }) { - + debugger const dayBoxes = fiveDayWeather.map((day) => { return
{day.weather}
diff --git a/app/components/Graphs.jsx b/app/components/Graphs.jsx index c94e167..564cea1 100644 --- a/app/components/Graphs.jsx +++ b/app/components/Graphs.jsx @@ -1,5 +1,4 @@ import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-sparklines"; -import { useSelector } from "react-redux"; import { v4 as uuidv4 } from "uuid"; export default function Graphs ({ fiveDayWeather }) { diff --git a/app/components/WeatherDetails.jsx b/app/components/WeatherDetails.jsx index b40fd35..6f3fd78 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/WeatherDetails.jsx @@ -21,12 +21,12 @@ export default function WeatherDetails ({ weatherDetails }) {
- +
-
+
- +
From 931a399c9e417cf1fdcdf3e05115f904586cd4c7 Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Mon, 12 Aug 2024 01:15:14 -0400 Subject: [PATCH 17/18] Finished project --- app/components/CurrentWeather.jsx | 22 ++++++++ app/components/CurrentWeatherDetails.jsx | 28 ----------- app/components/DayBox.jsx | 34 +++++++++++++ app/components/FiveDayWeather.jsx | 22 ++------ app/components/Graph.jsx | 19 +++++++ app/components/Graphs.jsx | 27 ++-------- app/components/Panel.jsx | 9 ++-- .../{WeatherDetails.jsx => Weather.jsx} | 6 +-- app/components/WeatherAPI.js | 50 ++++++++----------- app/components/useSearchBarValidation.js | 1 - app/globals.css | 30 +++++++++++ package-lock.json | 19 +++++++ package.json | 4 ++ 13 files changed, 165 insertions(+), 106 deletions(-) create mode 100644 app/components/CurrentWeather.jsx delete mode 100644 app/components/CurrentWeatherDetails.jsx create mode 100644 app/components/DayBox.jsx create mode 100644 app/components/Graph.jsx rename app/components/{WeatherDetails.jsx => Weather.jsx} (96%) diff --git a/app/components/CurrentWeather.jsx b/app/components/CurrentWeather.jsx new file mode 100644 index 0000000..4ecebf8 --- /dev/null +++ b/app/components/CurrentWeather.jsx @@ -0,0 +1,22 @@ +import Image from "next/image"; + +export default function CurrentWeather({ currentWeatherDetails }) { + + return ( +
+
+
{currentWeatherDetails.temp}°
+
{currentWeatherDetails.name}
+
{currentWeatherDetails.weather}
+
+ {`${currentWeatherDetails.weather} +
+ ) +} \ No newline at end of file diff --git a/app/components/CurrentWeatherDetails.jsx b/app/components/CurrentWeatherDetails.jsx deleted file mode 100644 index e68d9b1..0000000 --- a/app/components/CurrentWeatherDetails.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import Image from "next/image"; -import { useSelector } from "react-redux"; - -export default function CurrentWeatherDetails({id, currentWeatherDetails}) { - // const currentWeatherDetails = useSelector((state) => { - // return state.locations.locations.find((day) => day.id === id).currentWeather - // }); - - if (!currentWeatherDetails.name) return

Could not load location!

- - return ( -
-
-
{currentWeatherDetails.temp}°
-
{currentWeatherDetails.name}
-
{currentWeatherDetails.weather}
-
- {`${currentWeatherDetails.weather} -
- ) -} \ No newline at end of file diff --git a/app/components/DayBox.jsx b/app/components/DayBox.jsx new file mode 100644 index 0000000..655eec1 --- /dev/null +++ b/app/components/DayBox.jsx @@ -0,0 +1,34 @@ +import Image from "next/image"; +import {v4 as uuidv4} from "uuid"; +import { useEffect, useState } from "react"; + +export default function DayBox({ day }) { + const [width, setWidth] = useState(window.innerWidth); + + useEffect(() => { + const handleResize = () => { + setWidth(window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + + return ( +
+
{day.weather}
+
{day.temp}°
+ {`${day.weather} +
{day.day}
+
+ ); +} \ No newline at end of file diff --git a/app/components/FiveDayWeather.jsx b/app/components/FiveDayWeather.jsx index d9c1799..34a2743 100644 --- a/app/components/FiveDayWeather.jsx +++ b/app/components/FiveDayWeather.jsx @@ -1,25 +1,13 @@ import {v4 as uuidv4} from "uuid"; -import Image from "next/image"; +import DayBox from "./DayBox"; export default function FiveDayWeather({ fiveDayWeather }) { - debugger - const dayBoxes = fiveDayWeather.map((day) => { - return
-
{day.weather}
-
{day.temp}°
- {`${day.weather} -
{day.day}
-
- }); return (
- {dayBoxes} + { fiveDayWeather.map(day => + ) + }
- ) + ); } \ No newline at end of file diff --git a/app/components/Graph.jsx b/app/components/Graph.jsx new file mode 100644 index 0000000..7ebc3f5 --- /dev/null +++ b/app/components/Graph.jsx @@ -0,0 +1,19 @@ +import { v4 as uuidv4 } from "uuid"; +import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-sparklines"; + +export default function Graph({ data, name }) { + // calculate the rounded average of the values array + const average = data.reduce((sum, cur) => sum + cur, 0) / data.length; + const roundedAverage = Math.ceil(average * 100) / 100; + + return ( +
+ {name.toUpperCase()} + + + + + Avg: {roundedAverage} +
+ ) +} \ No newline at end of file diff --git a/app/components/Graphs.jsx b/app/components/Graphs.jsx index 564cea1..a300005 100644 --- a/app/components/Graphs.jsx +++ b/app/components/Graphs.jsx @@ -1,9 +1,8 @@ -import { Sparklines, SparklinesLine, SparklinesReferenceLine } from "react-sparklines"; import { v4 as uuidv4 } from "uuid"; +import Graph from "./Graph"; export default function Graphs ({ fiveDayWeather }) { - - // Reduce five day weather to only an object of temp, humidity, pressure arrays. + // Reduce five day weather to an object {temp: [], humidity:[], pressure:[]}. const graphData = fiveDayWeather.reduce((accum, day) => { accum.temp ? accum.temp.push(day.temp) : accum.temp = [day.temp]; accum.humidity ? accum.humidity.push(day.humidity) : accum.humidity = [day.humidity]; @@ -11,27 +10,11 @@ export default function Graphs ({ fiveDayWeather }) { return accum; }, {}); - // create graph components from temp, humidity, and pressure arrays - const graphs = Object.keys(graphData).map((graph) => { - // calculate the average of the values array - const average = graphData[graph].reduce((sum, cur) => sum + cur, 0) / graphData[graph].length; - const roundedAverage = Math.ceil(average * 100) / 100; - - return ( -
- {graph.toUpperCase()} - - - - - Avg: {roundedAverage} -
- ) - }) - return (
- {graphs} + {Object.keys(graphData).map(key => + + )}
) } \ No newline at end of file diff --git a/app/components/Panel.jsx b/app/components/Panel.jsx index 31cbb42..5c52744 100644 --- a/app/components/Panel.jsx +++ b/app/components/Panel.jsx @@ -1,13 +1,12 @@ -import WeatherDetails from "./WeatherDetails"; -import CurrentWeatherDetails from "./CurrentWeatherDetails"; -import { useSelector } from "react-redux"; +import Weather from "./Weather"; +import CurrentWeather from "./CurrentWeather"; export default function Panel({ weatherDetails}) { return (
- - + +
) } \ No newline at end of file diff --git a/app/components/WeatherDetails.jsx b/app/components/Weather.jsx similarity index 96% rename from app/components/WeatherDetails.jsx rename to app/components/Weather.jsx index 6f3fd78..b4c906b 100644 --- a/app/components/WeatherDetails.jsx +++ b/app/components/Weather.jsx @@ -1,11 +1,11 @@ import FiveDayWeather from "./FiveDayWeather"; -import { useSelector, useDispatch } from "react-redux"; +import { useDispatch } from "react-redux"; import { setDefaultLocation } from "../store/slices/locations"; import Graphs from "./Graphs"; import Modal from "./Modal"; -export default function WeatherDetails ({ weatherDetails }) { +export default function Weather ({ weatherDetails }) { const dispatch = useDispatch(); return ( @@ -54,5 +54,5 @@ export default function WeatherDetails ({ weatherDetails }) { />
- ) + ); } \ No newline at end of file diff --git a/app/components/WeatherAPI.js b/app/components/WeatherAPI.js index c48bcb2..6784374 100644 --- a/app/components/WeatherAPI.js +++ b/app/components/WeatherAPI.js @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; const APIKEY = "deecee58f4daa55a503c09ae97c1d3ab"; + const fetchData = async (url) => { const resp = await fetch(url); const data = await resp.json(); @@ -19,14 +20,14 @@ const getLatLonData = async function (cityName) { }; const getCurrentWeatherData = async function (lonLatData, unit="imperial") { - const {lon, lat, name,} = lonLatData; - const returnData = {} + const {lon, lat} = lonLatData; + const weatherData = {} const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=${unit}&appid=${APIKEY}` const data = await fetchData(url); - returnData.name = data.name; - returnData.temp = data.main.temp; - const {description, icon} = data.weather[0]; - const weatherData = {...returnData, weather: description, iconCode: icon}; + weatherData.name = data.name; + weatherData.temp = data.main?.temp; + weatherData.description = data?.weather[0]?.description; + weatherData.iconCode = data.weather[0]?.icon; return weatherData; }; @@ -44,28 +45,18 @@ const getRoundedAverage = value => Math.ceil((value / 8) * 100) / 100; const formatFiveDayData = function (data) { - let dateSeparatedArr = []; + let days = []; - // Slice every chunks of 8 items from data array and append to dateSeperatedArr + // Slice every chunks of 8 items from data array and append to days for (let i = 0; i < data.length; i += 8) { const chunk = data.slice(i, i + 8); - dateSeparatedArr.push(chunk); - } + days.push(chunk); + }; - - /** - * Formatting logic: - * Now that we have an array where each day is an index inside an array, - * the goal now is to reduce each index in the day ARRAY to be a single day OBJECT - * on every loop we += item temp to the temp of the accumulator (which is an obj), to be averaged later - * The first index of the day is used to get the day of the week and weather condition - * The last index of the day is used to calculated the average temp of that day - * - * from: dateSeparatedArray = [[day1data, day1data, day1data], [day2data, day2data], ...] - * to: dateSeparatedArray = [{day1Obj}, {day2Obj}, ...] - */ - dateSeparatedArr = dateSeparatedArr.map((dayArr) => { + // Re-assign days to be an array of objects + days = days.map((dayArr) => { return dayArr.reduce((accumulator, itm, dayIndex) => { + // Total temp, pressure, humidity to be averaged later accumulator.temp += itm.main.temp; accumulator.pressure += itm.main.pressure; accumulator.humidity += itm.main.humidity; @@ -89,21 +80,20 @@ const formatFiveDayData = function (data) { return accumulator; }, {"temp": 0, "pressure": 0, "humidity": 0, "weather": "", "iconCode": "", "day":""}) }) - return dateSeparatedArr; + return days; }; -const getWeather = async function ( cityName, latLon = null) { - console.log(cityName); +const getWeather = async function (cityName, latLon = null) { let latLonData; + + // If city name is not empty getLonLat data via apiCall else create latLonObject if (cityName !== "") { latLonData = await getLatLonData(cityName); - - if (Object.keys(latLonData).length === 0) { - return {} - } + if (Object.keys(latLonData).length === 0) {}; } else { latLonData = {lat: latLon.lat, lon: latLon.lon} } + const currentWeatherData = await getCurrentWeatherData(latLonData); const FiveDayWeatherData = await getFiveDayWeatherData(latLonData); diff --git a/app/components/useSearchBarValidation.js b/app/components/useSearchBarValidation.js index b3c5a26..9ac5c16 100644 --- a/app/components/useSearchBarValidation.js +++ b/app/components/useSearchBarValidation.js @@ -1,4 +1,3 @@ - import { useForm } from "react-hook-form"; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; diff --git a/app/globals.css b/app/globals.css index 00aef56..5e1d7f0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -12,3 +12,33 @@ html, body { max-width: 100vw; } + +.weather-degree { + font-size: 1.5rem; + font-weight: 500; +} + +.weather-city { + font-size: 1.5rem; + font-weight: 500; +} + +.weather-condition { + font-size: 1rem; + font-weight: 200; +} + +.weather-condition-sm { + font-size: 1rem; + font-weight: 500; +} + +.weather-degree-sm { + font-size: 1.25rem; + font-weight: 500; +} + +.weather-day { + font-size: 1.25rem; + font-weight: 200; +} diff --git a/package-lock.json b/package-lock.json index 3cd1ab4..bad8b7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,10 @@ "react-sparklines": "^1.7.0", "uuid": "^10.0.0", "yup": "^1.4.0" + }, + "devDependencies": { + "@types/react-sparklines": "^1.7.5", + "@types/uuid": "^10.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -529,6 +533,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-sparklines": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@types/react-sparklines/-/react-sparklines-1.7.5.tgz", + "integrity": "sha512-rIAmNyRKUqWWnaQMjNrxMNkgEFi5f9PrdczSNxj5DscAa48y4i9P0fRKZ72FmNcFsdg6Jx4o6CXWZtIaC0OJOg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", @@ -542,6 +555,12 @@ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", diff --git a/package.json b/package.json index cc6969c..1d756ea 100644 --- a/package.json +++ b/package.json @@ -23,5 +23,9 @@ "react-sparklines": "^1.7.0", "uuid": "^10.0.0", "yup": "^1.4.0" + }, + "devDependencies": { + "@types/react-sparklines": "^1.7.5", + "@types/uuid": "^10.0.0" } } From 77bfa3790525305d71ceda49342176888511ce31 Mon Sep 17 00:00:00 2001 From: Colormethanh Date: Mon, 12 Aug 2024 01:32:32 -0400 Subject: [PATCH 18/18] Edited README --- README.md | 40 ++++++++++++++++++++++++ app/components/useSearchBarValidation.js | 1 - 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 99d321a..e6e9bfa 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,43 @@ This project has been created by a student at Parsity, an online software engine If you have any questions about this project or the program in general, visit [parsity.io](https://parsity.io/) or email hello@parsity.io. +# What is it? + +A React weather app that fetches weather data using the [open weather api](https://openweathermap.org/) and stores the data using redux. + +## Features + +- Search for the weather data of a city using a text input and receive back current weather, 5 day forecast, and 5 day forecast in graph form. +- Search for the weather of current location using a button +- Set a default location to local storage +- Responsive components + +## Component structure + +``` +Layout +| +|AppNavBar.js +| +|page.js + | + |SearchBar.jsx + | | + | |SearchBarForm.jsx + | + |WeatherPanel.jsx + | + |Panel.jsx + | | + | |CurrentWeather.jsx + | + |Weather.jsx + | + |FiveDayWeather.jsx + | | + | |DayBox.jsx + | + |Graphs.jsx + | + |Graph.jsx +``` diff --git a/app/components/useSearchBarValidation.js b/app/components/useSearchBarValidation.js index 9ac5c16..ba3ca0f 100644 --- a/app/components/useSearchBarValidation.js +++ b/app/components/useSearchBarValidation.js @@ -2,7 +2,6 @@ import { useForm } from "react-hook-form"; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; - export default function useFormValidation() { const formSchema = yup .object({