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 (
+
+
+
+ );
+};
+
+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
-
-
-
-
-
-
-
-
-
-
- )
-}
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"]
+}