diff --git a/app/components/renderHumidity.tsx b/app/components/renderHumidity.tsx new file mode 100644 index 0000000..660cfcc --- /dev/null +++ b/app/components/renderHumidity.tsx @@ -0,0 +1,47 @@ +import { useSelector } from "react-redux"; +import { WeatherData, WeatherContainer } from "../store/slices/weather"; +import { + Sparklines, + SparklinesLine, + SparklinesReferenceLine, +} from "react-sparklines"; + +export default function RenderHumidity() { + const conditions = useSelector((state: WeatherData) => state.weather.weather); //pulls current state in redux store + + const findHumidity = (condition: WeatherContainer) => { + return condition?.list?.map?.( + (weather: WeatherContainer) => weather.main.humidity + ); //takes the humidity over the next 5 days + }; + + return ( +
+ {conditions?.map((condition: WeatherContainer, index: number) => { + const humidityArray = findHumidity(condition); //sets that humidity in an array + + const sum = humidityArray?.reduce( + (accumulator: number, currentValue: number) => { + return accumulator + currentValue; + }, + 0 + ); + const averageTotal = sum / humidityArray?.length; //finds average humidity + const humidityAverage = Math.round(averageTotal); //rounds to whole number + + if (condition === undefined) { + return; //if no conditions are passed through, do not return anything + } + return ( +
+ + + + +

{humidityAverage}%

+
+ ); + })} +
+ ); +} diff --git a/app/components/renderPressure.tsx b/app/components/renderPressure.tsx new file mode 100644 index 0000000..48ecc52 --- /dev/null +++ b/app/components/renderPressure.tsx @@ -0,0 +1,47 @@ +import { useSelector } from "react-redux"; +import { WeatherData, WeatherContainer } from "../store/slices/weather"; +import { + Sparklines, + SparklinesLine, + SparklinesReferenceLine, +} from "react-sparklines"; + +export default function RenderPressure() { + const conditions = useSelector((state: WeatherData) => state.weather.weather); //pulls current state in redux store + + const findPressure = (condition: WeatherContainer) => { + return condition?.list?.map?.( + (weather: WeatherContainer) => weather.main.pressure + ); //takes the pressure over the next 5 days + }; + + return ( +
+ {conditions?.map((condition: WeatherContainer, index: number) => { + const pressureArray = findPressure(condition); //sets that pressure in an array + + const sum = pressureArray?.reduce( + (accumulator: number, currentValue: number) => { + return accumulator + currentValue; + }, + 0 + ); + const averageTotal = sum / pressureArray?.length; //finds average pressure + const pressureAverage = Math.round(averageTotal); //rounds to whole number + + if (condition === undefined) { + return; //if no conditions are passed through, do not return anything + } + return ( +
+ + + + +

{pressureAverage}hPa

+
+ ); + })} +
+ ); +} diff --git a/app/components/renderTemp.tsx b/app/components/renderTemp.tsx new file mode 100644 index 0000000..f40525e --- /dev/null +++ b/app/components/renderTemp.tsx @@ -0,0 +1,47 @@ +import { useSelector } from "react-redux"; +import { WeatherData, WeatherContainer } from "../store/slices/weather"; +import { + Sparklines, + SparklinesLine, + SparklinesReferenceLine, +} from "react-sparklines"; + +export default function RenderTemp() { + const conditions = useSelector((state: WeatherData) => state.weather.weather); //pulls current state in redux store + + const findTemp = (condition: WeatherContainer) => { + return condition?.list?.map?.( + (weather: WeatherContainer) => weather.main.temp + ); //takes the temperature over the next 5 days + }; + + return ( +
+ {conditions?.map((condition: WeatherContainer, index: number) => { + const tempArray = findTemp(condition); //sets that temp in an array + + const sum = tempArray?.reduce( + (accumulator: number, currentValue: number) => { + return accumulator + currentValue; + }, + 0 + ); + const averageTotal = sum / tempArray?.length; //finds average temperature + const tempAverage = Math.round(averageTotal); //rounds to whole number + + if (condition === undefined) { + return; //if no conditions are passed through, do not return anything + } + return ( +
+ + + + +

{tempAverage}ºF

+
+ ); + })} +
+ ); +} diff --git a/app/components/renderWeather.tsx b/app/components/renderWeather.tsx new file mode 100644 index 0000000..63feb28 --- /dev/null +++ b/app/components/renderWeather.tsx @@ -0,0 +1,27 @@ +import RenderHumidity from "./renderHumidity"; +import RenderPressure from "./renderPressure"; +import RenderTemp from "./renderTemp"; +import SearchCity from "./searchCity"; + +const RenderWeather = () => { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ); +}; + +export default RenderWeather; diff --git a/app/components/searchBar.tsx b/app/components/searchBar.tsx new file mode 100644 index 0000000..a632731 --- /dev/null +++ b/app/components/searchBar.tsx @@ -0,0 +1,39 @@ +"use client"; +import styles from "../page.module.css"; +import { useState } from "react"; +import { useDispatch } from "react-redux"; +import { fetchLocation } from "../store/slices/locations"; +import { AppDispatch } from "../store/configureStore"; + +interface SearchBarProps { + placeholder: string; +} + +const SearchBar = ({ placeholder}: SearchBarProps): JSX.Element => { + const dispatch = useDispatch(); //allows store to listen to events + const [query, setQuery] = useState(""); //allows query to be set as state + + const handleSubmit = (e: React.FormEvent): void => { + e.preventDefault(); //prevents running default requests when submitted + setQuery(""); //reverts to empty search field + dispatch(fetchLocation(query)); + }; + return ( +
+
+ ): void => { + setQuery(e.target.value); + }} + /> + +
+
+ ); +}; + +export default SearchBar; diff --git a/app/components/searchCity.tsx b/app/components/searchCity.tsx new file mode 100644 index 0000000..749c455 --- /dev/null +++ b/app/components/searchCity.tsx @@ -0,0 +1,34 @@ +"use client"; +import { useSelector, useDispatch } from "react-redux"; +import { LocationParams } from "../store/slices/locations"; +import { useEffect } from "react"; +import { AppDispatch } from "../store/configureStore"; +import { fetchWeather } from "../store/slices/weather"; + +export default function SearchCity() { + const locations = useSelector( + (state: LocationParams) => state.location.location + ); + + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchWeather(locations)); + }, [dispatch, locations]); + + return locations?.map((location: LocationParams, index: number) => { + if (locations[locations.length - 1][0] === undefined) { + alert("Please enter a valid city name"); + return; + // trying to address edge casing or invalid entry, alert is mapped in response but I could not figure out how to only alert once. + } + return ( +
+
+
+

{location[location.length - 1].name}

+
+
+
+ ); + }); +} diff --git a/app/globals.css b/app/globals.css index d4f491e..f4bd77c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,9 +1,9 @@ :root { --max-width: 1100px; --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', - 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', - 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; diff --git a/app/layout.js b/app/layout.js index c93f806..43605c9 100644 --- a/app/layout.js +++ b/app/layout.js @@ -1,17 +1,17 @@ -import './globals.css' -import { Inter } from 'next/font/google' +'use client' +import { Inter } from 'next/font/google'; +import { Provider } from 'react-redux'; +import store from './store/configureStore'; const inter = Inter({ subsets: ['latin'] }) -export const metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} export default function RootLayout({ children }) { return ( - {children} + + {children} + ) } diff --git a/app/page.js b/app/page.js deleted file mode 100644 index f049c39..0000000 --- a/app/page.js +++ /dev/null @@ -1,95 +0,0 @@ -import Image from 'next/image' -import styles from './page.module.css' - -export default function Home() { - return ( -
-
-

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

- -
- -
- Next.js Logo -
- - -
- ) -} diff --git a/app/page.module.css b/app/page.module.css index 6676d2c..ad2ff56 100644 --- a/app/page.module.css +++ b/app/page.module.css @@ -1,3 +1,16 @@ +.button { + height: auto; + width: auto; + background-color: blue; + color: white; + border-radius: 5px; + margin: 10px; +} + +.button:hover { + background-color: lightblue; +} + .main { display: flex; flex-direction: column; @@ -51,7 +64,9 @@ 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; + transition: + background 200ms, + border 200ms; } .card span { @@ -97,7 +112,7 @@ .center::before, .center::after { - content: ''; + content: ""; left: 50%; position: absolute; filter: blur(45px); diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..c84f825 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,40 @@ +"use client"; +import styles from "./page.module.css"; +import "bootstrap/dist/css/bootstrap.min.css"; +import SearchBar from "./components/searchBar"; +import RenderWeather from "./components/renderWeather"; + +export default function App(): JSX.Element { + return ( +
+
+
+
+

Weather App

+
+ +
+
+
+
City
+
+
+
Temperature (ºF)
+
+
+
Humidity (%)
+
+
+
Pressure (hPa)
+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/app/store/configureStore.tsx b/app/store/configureStore.tsx new file mode 100644 index 0000000..8732c88 --- /dev/null +++ b/app/store/configureStore.tsx @@ -0,0 +1,10 @@ +import { configureStore } from "@reduxjs/toolkit"; +import rootReducer from "../store/rootReducer"; + +const store = configureStore({ + reducer: rootReducer, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; +export default store; diff --git a/app/store/rootReducer.tsx b/app/store/rootReducer.tsx new file mode 100644 index 0000000..a28fc7e --- /dev/null +++ b/app/store/rootReducer.tsx @@ -0,0 +1,10 @@ +import { combineReducers } from "redux"; +import locationsReducer from "./slices/locations"; +import weatherReducer from "./slices/weather"; + +const rootReducer = combineReducers({ + location: locationsReducer, + weather: weatherReducer, +}); + +export default rootReducer; diff --git a/app/store/slices/locations.tsx b/app/store/slices/locations.tsx new file mode 100644 index 0000000..2500bde --- /dev/null +++ b/app/store/slices/locations.tsx @@ -0,0 +1,65 @@ +import axios from "axios"; +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; + +const API_KEY: string = process.env.NEXT_PUBLIC_WEATHER_API_KEY; + +export const fetchLocation = createAsyncThunk( + "location/fetchLocation", + async (query: QueryPayload) => { + if (query.trim() === "") { + return; // Don't make the API request if the query is empty + } + //query is whatever is searched in SearchBar + const response = await axios.get( + `http://api.openweathermap.org/geo/1.0/direct?q=${query}&appid=${API_KEY}` + ); + return response.data; + } +); + +type QueryPayload = string; + +export interface LocationParams { + name: string; + lat: number; + lon: number; + length: number; + location: any; +} + +interface FetchStatus { + location: LocationParams[]; + loading: boolean; + error: string | null; +} + +const initialState: FetchStatus = { + location: [], + loading: false, + error: null, +}; + +export const locationsSlice = createSlice({ + name: "location", + initialState, //state before reducer is used + reducers: {}, + + extraReducers: (builder) => { + builder.addCase(fetchLocation.pending, (state) => { + state.loading = true; + }); + builder.addCase( + fetchLocation.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.location.push(action.payload); + } + ); + builder.addCase(fetchLocation.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message; + }); + }, +}); + +export default locationsSlice.reducer; diff --git a/app/store/slices/weather.tsx b/app/store/slices/weather.tsx new file mode 100644 index 0000000..f1b9527 --- /dev/null +++ b/app/store/slices/weather.tsx @@ -0,0 +1,78 @@ +import axios from "axios"; +import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; +import { LocationParams } from "./locations"; + +const API_KEY: string = process.env.NEXT_PUBLIC_WEATHER_API_KEY; + +export const fetchWeather = createAsyncThunk( + "weather/fetchWeather", + async (location: LocationParams) => { + // console.log("location:", location[length - 1][length - 1]); + const lastLocation = location[location.length - 1][0]; + + if (location === undefined) { + return; // Don't make the API request if the query is empty + } + const response = await axios.get( + `http://api.openweathermap.org/data/2.5/forecast?lat=${lastLocation.lat}&lon=${lastLocation.lon}&appid=${API_KEY}&units=imperial` + ); + return response.data; + } +); + +export interface WeatherContainer { + //this is what the weather data is stored in the api call + main: WeatherData; + dt: number; + list: any; +} + +export interface WeatherData { + temp: number; + pressure: number; + humidity: number; + weather: any; +} + +interface TempData { + tempArray: []; +} + +interface FetchStatus { + weather: WeatherData[]; + temp: TempData[]; + loading: boolean; + error: string | null; +} + +const initialState: FetchStatus = { + weather: [], + temp: [], + loading: false, + error: null, +}; + +export const weatherSlice = createSlice({ + name: "weather", + initialState, //state before reducer is used + reducers: {}, + + extraReducers: (builder) => { + builder.addCase(fetchWeather.pending, (state) => { + state.loading = true; + }); + builder.addCase( + fetchWeather.fulfilled, + (state, action: PayloadAction) => { + state.loading = false; + state.weather.push(action.payload); + } + ); + builder.addCase(fetchWeather.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message; + }); + }, +}); + +export default weatherSlice.reducer; diff --git a/package-lock.json b/package-lock.json index 90f6bb1..ca484d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,22 @@ "name": "parsity_rtk_weather", "version": "0.1.0", "dependencies": { + "@reduxjs/toolkit": "^2.2.7", + "axios": "^1.7.5", + "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" + "react-dom": "18.2.0", + "react-redux": "^9.1.2", + "react-sparklines": "^1.7.0", + "redux": "^5.0.1" + }, + "devDependencies": { + "@types/node": "^22.5.0", + "@types/react": "^18.3.4", + "typescript": "^5.5.4" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -296,6 +307,39 @@ "node": ">= 8" } }, + "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==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "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/@rushstack/eslint-patch": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.5.0.tgz", @@ -314,6 +358,36 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/node": { + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "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==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "devOptional": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "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/@typescript-eslint/parser": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", @@ -615,6 +689,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -634,6 +713,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -647,6 +736,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", @@ -753,6 +860,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -771,6 +889,12 @@ "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==", + "devOptional": true + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -826,6 +950,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1491,6 +1623,25 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1499,6 +1650,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1759,6 +1923,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", @@ -2270,6 +2443,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2628,6 +2820,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -2683,6 +2880,53 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-redux": { + "version": "9.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-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/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 +2967,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", @@ -3191,10 +3440,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "peer": true, + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3217,6 +3465,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3225,6 +3479,14 @@ "punycode": "^2.1.0" } }, + "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/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 6ca0f8f..62bf12c 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,21 @@ "lint": "next lint" }, "dependencies": { + "@reduxjs/toolkit": "^2.2.7", + "axios": "^1.7.5", + "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" + "react-dom": "18.2.0", + "react-redux": "^9.1.2", + "react-sparklines": "^1.7.0", + "redux": "^5.0.1" + }, + "devDependencies": { + "@types/node": "^22.5.0", + "@types/react": "^18.3.4", + "typescript": "^5.5.4" } } diff --git a/public/data.json b/public/data.json new file mode 100644 index 0000000..531e816 --- /dev/null +++ b/public/data.json @@ -0,0 +1,39 @@ +[ + { + "name": "Austin", + "local_names": { + "it": "Austin", + "sr": "Остин", + "el": "Ώστιν", + "hi": "ऑस्टिन", + "gr": "Αὐγούστα", + "pl": "Austin", + "uk": "Остін", + "ta": "ஆஸ்டின்", + "fr": "Austin", + "ru": "Остин", + "bn": "অস্টিন", + "ar": "أوستن", + "te": "ఆస్టిన్", + "fa": "آستین", + "en": "Austin", + "es": "Austin", + "de": "Austin", + "zh": "奥斯汀 / 柯士甸", + "ku": "Austin", + "be": "Остын", + "he": "אוסטין", + "ur": "آسٹن", + "ja": "オースティン", + "vi": "Austin", + "tr": "Austin", + "eo": "Aŭstino", + "ko": "오스틴", + "pt": "Austin" + }, + "lat": 30.2711286, + "lon": -97.7436995, + "country": "US", + "state": "Texas" + } +] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d9248fc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + "app/store/slices/search.tsx", + "app/store/slices/weather.tsx", + "app/page.tsx", + "app/store/slices/locations.tsx" + ], + "exclude": ["node_modules"] +}