diff --git a/package-lock.json b/package-lock.json index fa257a4..bfcd607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -461,6 +461,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -509,6 +510,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1151,8 +1153,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/chai": { "version": "5.2.3", @@ -1221,6 +1222,7 @@ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.3.0" } @@ -1584,7 +1586,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1595,7 +1596,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -1609,7 +1609,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -1869,7 +1868,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -1889,8 +1887,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.5.428", @@ -2806,7 +2803,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -2997,7 +2993,6 @@ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3066,7 +3061,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3121,6 +3115,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.28.0" }, @@ -3133,8 +3128,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/require-from-string": { "version": "2.0.2", diff --git a/src/App.tsx b/src/App.tsx index 4fa6799..27b10c2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,14 +2,19 @@ import { useState } from "react" import "./App.css" import LocationList from "./components/location-list" import ViewCity from "./components/ViewCity" +import SearchBar from "./components/SearchBar" function App() { const [selectedCity, setSelectedCity] = useState("Sørumsand") + const handleSearch = (query: string) => { + setSelectedCity(query) + } return ( <>
+
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx new file mode 100644 index 0000000..4399445 --- /dev/null +++ b/src/components/SearchBar.tsx @@ -0,0 +1,118 @@ +import { useState } from "react" +import { useQuery } from "@tanstack/react-query" +import type { quickCityResult } from "../hooks/fetchCities/types" + +type SearchBarProps = { + onSearch: (query: string) => void +} + +export default function SearchBar({ onSearch }: SearchBarProps) { + const [value, setValue] = useState("") + + const { data } = useQuery({ + queryKey: ["citySuggestions", value], + queryFn: () => + fetch( + `https://geocoding-api.open-meteo.com/v1/search?name=${value}&count=4&language=no` + ).then((res) => res.json()), + enabled: value.length >= 2, + }) + const results = data?.results ?? [] + const handleSubmit = (e: React.SubmitEvent) => { + e.preventDefault() + + if (!value.trim()) return + + onSearch(value.trim()) + } + + const countryCodeToFlag = (code: string) => { + if (!code) return "🌍" + + return code + .toUpperCase() + .split("") + .map((c) => String.fromCodePoint(127397 + c.charCodeAt(0))) + .join("") + } + + return ( +
+
+ setValue(e.target.value)} + style={{ + width: "80%", + padding: "12px 18px", + borderRadius: "999px", + border: "none", + outline: "none", + fontSize: "1rem", + backgroundColor: "#ffffff", + boxShadow: "0 4px 12px rgba(0,0,0,0.12)", + }} + /> +
+ + {results.length > 0 && ( +
+ {results.map((city: quickCityResult) => ( + + ))} +
+ )} +
+ ) +} diff --git a/src/components/ViewCity.tsx b/src/components/ViewCity.tsx index 8a5cb18..0940353 100644 --- a/src/components/ViewCity.tsx +++ b/src/components/ViewCity.tsx @@ -22,12 +22,15 @@ export default function ViewCity(arg: ViewCityProp) { const firstTimeStep = result?.properties.timeseries[0] const windSpeed = firstTimeStep?.data.instant.details?.wind_speed const temperature = firstTimeStep?.data.instant.details?.air_temperature + const country = data.result[0]?.country return (
-
Været i {arg.city}
-

Temperatur: {temperature}

-

Vindhastighet: {windSpeed}

+
+ Været i {arg.city}, {country} +
+

Temperatur: {temperature}°C

+

Vindhastighet: {windSpeed} m/s

) } diff --git a/src/hooks/fetchCities/types.ts b/src/hooks/fetchCities/types.ts index bd79d27..2ef89c5 100644 --- a/src/hooks/fetchCities/types.ts +++ b/src/hooks/fetchCities/types.ts @@ -5,9 +5,21 @@ export type citySearchResult = { longitude: number elevation: number timezone: string + country: string + country_code: string }[] } +export type quickCityResult = { + name: string + latitude: number + longitude: number + elevation: number + timezone: string + country: string + country_code: string +} + export const countryCodes = [ "AU", "AT", diff --git a/src/hooks/fetchCities/useGetCityCoordinates.ts b/src/hooks/fetchCities/useGetCityCoordinates.ts index 6199fd7..633307e 100644 --- a/src/hooks/fetchCities/useGetCityCoordinates.ts +++ b/src/hooks/fetchCities/useGetCityCoordinates.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query" import type { citySearchResult, citySearchFilter } from "./types" const useGetCityCoordinates = (filter: citySearchFilter) => { - const queryString = `?name=${filter.cityName}${filter.countryCode ? `&countryCode=${filter.countryCode}` : ""}` + const queryString = `?name=${filter.cityName}${filter.countryCode ? `&countryCode=${filter.countryCode}` : ""}&language=no` const { isLoading, error, data } = useQuery({ queryKey: ["citySearch", filter.cityName], diff --git a/tests/components/ViewCity.test.tsx b/tests/components/ViewCity.test.tsx index c5ec3e4..8c7e45a 100644 --- a/tests/components/ViewCity.test.tsx +++ b/tests/components/ViewCity.test.tsx @@ -45,9 +45,9 @@ describe("ViewCity", () => { render() - expect(screen.getByText("Været i Oslo")).toBeTruthy() - expect(screen.getByText("Temperatur: 12.5")).toBeTruthy() - expect(screen.getByText("Vindhastighet: 4.2")).toBeTruthy() + expect(screen.getByText("Været i Oslo,")).toBeTruthy() + expect(screen.getByText("Temperatur: 12.5°C")).toBeTruthy() + expect(screen.getByText("Vindhastighet: 4.2 m/s")).toBeTruthy() expect(mockUseGetCityCoordinates).toHaveBeenCalledWith({ cityName: "Oslo" }) expect(mockUseGetWeather).toHaveBeenCalledWith(59.9, 10.7) }) diff --git a/tests/hooks/fetchCities/useGetCityCoordinates.test.ts b/tests/hooks/fetchCities/useGetCityCoordinates.test.ts index c0d2790..ca6f7b7 100644 --- a/tests/hooks/fetchCities/useGetCityCoordinates.test.ts +++ b/tests/hooks/fetchCities/useGetCityCoordinates.test.ts @@ -29,6 +29,8 @@ it("should fetch the city search results", async () => { longitude: 69, elevation: 420, timezone: "Europe/Oslo", + country: "Norge", + country_code: "NO", }, { name: "Oslo", @@ -36,6 +38,8 @@ it("should fetch the city search results", async () => { longitude: 0, elevation: 10, timezone: "America/Chicago", + country: "USA", + country_code: "US", }, ], } @@ -61,6 +65,6 @@ it("should fetch the city search results", async () => { expect(fetchMock).toHaveBeenCalledTimes(1) expect(fetchMock).toHaveBeenCalledWith( - "https://geocoding-api.open-meteo.com/v1/search?name=Oslo" + "https://geocoding-api.open-meteo.com/v1/search?name=Oslo&language=no" ) })