From d81a7abcce5300051399eaadb7ed0fc71fdf2719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Tue, 15 Sep 2026 18:48:42 +0200 Subject: [PATCH 1/3] Fetch weather and city search --- "v\303\246r/package-lock.json" | 27 +++ "v\303\246r/package.json" | 1 + "v\303\246r/src/App.tsx" | 4 + "v\303\246r/src/lib/fetchWeather/types.ts" | 178 ++++++++++++++++++ .../src/lib/fetchWeather/useGetWeather.ts" | 13 ++ "v\303\246r/src/lib/useGetCityCoordinates.ts" | 24 +++ "v\303\246r/src/main.tsx" | 13 +- 7 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 "v\303\246r/src/lib/fetchWeather/types.ts" create mode 100644 "v\303\246r/src/lib/fetchWeather/useGetWeather.ts" create mode 100644 "v\303\246r/src/lib/useGetCityCoordinates.ts" diff --git "a/v\303\246r/package-lock.json" "b/v\303\246r/package-lock.json" index 909fe61..3b7ca00 100644 --- "a/v\303\246r/package-lock.json" +++ "b/v\303\246r/package-lock.json" @@ -8,6 +8,7 @@ "name": "var.json", "version": "0.0.0", "dependencies": { + "@tanstack/react-query": "^5.102.8", "react": "^19.2.8", "react-dom": "^19.2.8" }, @@ -848,6 +849,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@tanstack/query-core": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", + "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", + "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", diff --git "a/v\303\246r/package.json" "b/v\303\246r/package.json" index a151cda..cc694f8 100644 --- "a/v\303\246r/package.json" +++ "b/v\303\246r/package.json" @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@tanstack/react-query": "^5.102.8", "react": "^19.2.8", "react-dom": "^19.2.8" }, diff --git "a/v\303\246r/src/App.tsx" "b/v\303\246r/src/App.tsx" index 9ca8e8d..e72320c 100644 --- "a/v\303\246r/src/App.tsx" +++ "b/v\303\246r/src/App.tsx" @@ -3,9 +3,12 @@ import heroImg from './assets/hero.png' import reactLogo from './assets/react.svg' import viteLogo from './assets/vite.svg' import './App.css' +import useGetWeather from './lib/fetchWeather/useGetWeather' function App() { const [count, setCount] = useState(0) + // Example fetch, plz remove asap + const { result } = useGetWeather(67, 67, 420) return ( <> @@ -16,6 +19,7 @@ function App() { Vite logo
+ {JSON.stringify(result)}

Get started

Edit src/App.tsx and save to test HMR diff --git "a/v\303\246r/src/lib/fetchWeather/types.ts" "b/v\303\246r/src/lib/fetchWeather/types.ts" new file mode 100644 index 0000000..702841c --- /dev/null +++ "b/v\303\246r/src/lib/fetchWeather/types.ts" @@ -0,0 +1,178 @@ +export const weatherSymbolCodes = [ + "clearsky_day", + "clearsky_night", + "clearsky_polartwilight", + "fair_day", + "fair_night", + "fair_polartwilight", + "lightssnowshowersandthunder_day", + "lightssnowshowersandthunder_night", + "lightssnowshowersandthunder_polartwilight", + "lightsnowshowers_day", + "lightsnowshowers_night", + "lightsnowshowers_polartwilight", + "heavyrainandthunder", + "heavysnowandthunder", + "rainandthunder", + "heavysleetshowersandthunder_day", + "heavysleetshowersandthunder_night", + "heavysleetshowersandthunder_polartwilight", + "heavysnow", + "heavyrainshowers_day", + "heavyrainshowers_night", + "heavyrainshowers_polartwilight", + "lightsleet", + "heavyrain", + "lightrainshowers_day", + "lightrainshowers_night", + "lightrainshowers_polartwilight", + "heavysleetshowers_day", + "heavysleetshowers_night", + "heavysleetshowers_polartwilight", + "lightsleetshowers_day", + "lightsleetshowers_night", + "lightsleetshowers_polartwilight", + "snow", + "heavyrainshowersandthunder_day", + "heavyrainshowersandthunder_night", + "heavyrainshowersandthunder_polartwilight", + "snowshowers_day", + "snowshowers_night", + "snowshowers_polartwilight", + "fog", + "snowshowersandthunder_day", + "snowshowersandthunder_night", + "snowshowersandthunder_polartwilight", + "lightsnowandthunder", + "heavysleetandthunder", + "lightrain", + "rainshowersandthunder_day", + "rainshowersandthunder_night", + "rainshowersandthunder_polartwilight", + "rain", + "lightsnow", + "lightrainshowersandthunder_day", + "lightrainshowersandthunder_night", + "lightrainshowersandthunder_polartwilight", + "heavysleet", + "sleetandthunder", + "lightrainandthunder", + "sleet", + "lightssleetshowersandthunder_day", + "lightssleetshowersandthunder_night", + "lightssleetshowersandthunder_polartwilight", + "lightsleetandthunder", + "partlycloudy_day", + "partlycloudy_night", + "partlycloudy_polartwilight", + "sleetshowersandthunder_day", + "sleetshowersandthunder_night", + "sleetshowersandthunder_polartwilight", + "rainshowers_day", + "rainshowers_night", + "rainshowers_polartwilight", + "snowandthunder", + "sleetshowers_day", + "sleetshowers_night", + "sleetshowers_polartwilight", + "cloudy", + "heavysnowshowersandthunder_day", + "heavysnowshowersandthunder_night", + "heavysnowshowersandthunder_polartwilight", + "heavysnowshowers_day", + "heavysnowshowers_night", + "heavysnowshowers_polartwilight" +] as const + +export type WeatherSymbolCode = (typeof weatherSymbolCodes)[number] + +export type PointGeometry = { + coordinates: [longitude: number, latitude: number, altitude: number], + type: "Point" +} + +export type ForecastUnits = { + air_pressure_at_sea_level?: string, + air_temperature?: string, + air_temperature_max?: string, + air_temperature_min?: string, + cloud_area_fraction?: string, + cloud_area_fraction_high?: string, + cloud_area_fraction_low?: string, + cloud_area_fraction_medium?: string, + dew_point_temperature?: string, + fog_area_fraction?: string, + precipitation_amount?: string, + precipitation_amount_max?: string, + precipitation_amount_min?: string, + probability_of_precipitation?: string, + probability_of_thunder?: string, + relative_humidity?: string, + ultraviolet_index_clear_sky_max?: string, + wind_from_direction?: string, + wind_speed?: string, + wind_speed_of_gust?: string +} + +export type ForecastTimeInstant = { + air_pressure_at_sea_level?: number, + air_temperature?: number, + cloud_area_fraction?: number, + cloud_area_fraction_high?: number, + cloud_area_fraction_low?: number, + cloud_area_fraction_medium?: number, + dew_point_temperature?: number, + fog_area_fraction?: number, + relative_humidity?: number, + wind_from_direction?: number, + wind_speed?: number, + wind_speed_of_gust?: number +} + +export type ForecastTimePeriod = { + air_temperature_max?: number, + air_temperature_min?: number, + precipitation_amount?: number, + precipitation_amount_max?: number, + precipitation_amount_min?: number, + probability_of_precipitation?: number, + probability_of_thunder?: number, + ultraviolet_index_clear_sky_max?: number +} + +export type ForecastSummary = { + symbol_code: WeatherSymbolCode +} + +export type ForecastPeriod = { + details: ForecastTimePeriod, + summary: ForecastSummary +} + +export type ForecastTimeStep = { + data: { + instant: { + details?: ForecastTimeInstant + }, + next_1_hours?: ForecastPeriod, + next_6_hours?: ForecastPeriod, + next_12_hours?: ForecastPeriod + }, + time: string +} + +export type Forecast = { + meta: { + units: ForecastUnits, + updated_at: string + }, + timeseries: ForecastTimeStep[] +} + +export type METJSONForecast = { + geometry: PointGeometry, + properties: Forecast, + type: "Feature" +} + +export type WeatherApiResponse = METJSONForecast diff --git "a/v\303\246r/src/lib/fetchWeather/useGetWeather.ts" "b/v\303\246r/src/lib/fetchWeather/useGetWeather.ts" new file mode 100644 index 0000000..eefd174 --- /dev/null +++ "b/v\303\246r/src/lib/fetchWeather/useGetWeather.ts" @@ -0,0 +1,13 @@ +import { useQuery } from "@tanstack/react-query" +import { type WeatherApiResponse } from "./types" + +const useGetWeather = (lat: number, lon: number, alt?: number) => { + const { isLoading, error, data } = useQuery({ + queryKey: ["citySearch", `${lat}-${lon}`], + queryFn: () => fetch(`https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${lat}&lon=${lon}${alt ? `&altitude=${alt}` : ""}`).then((res) => res.json() as Promise) + }) + + return { isLoading, error, result: data } +} + +export default useGetWeather diff --git "a/v\303\246r/src/lib/useGetCityCoordinates.ts" "b/v\303\246r/src/lib/useGetCityCoordinates.ts" new file mode 100644 index 0000000..f5a9003 --- /dev/null +++ "b/v\303\246r/src/lib/useGetCityCoordinates.ts" @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query" + +export type citySearchResult = { + result: { + name: string, + latitude: number, + longitude: number + elevation: number, + timezone: string + }[] +} + +const useGetCityCoordinates = (cityName: string) => { + const { isLoading, error, data } = useQuery({ + queryKey: ["citySearch", cityName], + queryFn: () => fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${cityName}`).then((res) => res.json() as Promise) + }) + + const result = data?.result ?? [] + + return { isLoading, error, result } +} + +export default useGetCityCoordinates diff --git "a/v\303\246r/src/main.tsx" "b/v\303\246r/src/main.tsx" index bef5202..34913a4 100644 --- "a/v\303\246r/src/main.tsx" +++ "b/v\303\246r/src/main.tsx" @@ -2,9 +2,16 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { + QueryClient, QueryClientProvider +} from '@tanstack/react-query' + +const queryClient = new QueryClient() createRoot(document.getElementById('root')!).render( - - - , + + + + + , ) From 90f6a975dc9b1988405811025fd7ea02451d1217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Tue, 15 Sep 2026 18:53:05 +0200 Subject: [PATCH 2/3] hooks --- "v\303\246r/src/App.tsx" | 2 +- .../types.ts" => "v\303\246r/src/hooks/fetchWeather/types.ts" | 0 .../src/hooks/fetchWeather/useGetWeather.ts" | 0 .../src/hooks/useGetCityCoordinates.ts" | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename "v\303\246r/src/lib/fetchWeather/types.ts" => "v\303\246r/src/hooks/fetchWeather/types.ts" (100%) rename "v\303\246r/src/lib/fetchWeather/useGetWeather.ts" => "v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" (100%) rename "v\303\246r/src/lib/useGetCityCoordinates.ts" => "v\303\246r/src/hooks/useGetCityCoordinates.ts" (100%) diff --git "a/v\303\246r/src/App.tsx" "b/v\303\246r/src/App.tsx" index e72320c..8ef557e 100644 --- "a/v\303\246r/src/App.tsx" +++ "b/v\303\246r/src/App.tsx" @@ -3,7 +3,7 @@ import heroImg from './assets/hero.png' import reactLogo from './assets/react.svg' import viteLogo from './assets/vite.svg' import './App.css' -import useGetWeather from './lib/fetchWeather/useGetWeather' +import useGetWeather from './hooks/fetchWeather/useGetWeather' function App() { const [count, setCount] = useState(0) diff --git "a/v\303\246r/src/lib/fetchWeather/types.ts" "b/v\303\246r/src/hooks/fetchWeather/types.ts" similarity index 100% rename from "v\303\246r/src/lib/fetchWeather/types.ts" rename to "v\303\246r/src/hooks/fetchWeather/types.ts" diff --git "a/v\303\246r/src/lib/fetchWeather/useGetWeather.ts" "b/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" similarity index 100% rename from "v\303\246r/src/lib/fetchWeather/useGetWeather.ts" rename to "v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" diff --git "a/v\303\246r/src/lib/useGetCityCoordinates.ts" "b/v\303\246r/src/hooks/useGetCityCoordinates.ts" similarity index 100% rename from "v\303\246r/src/lib/useGetCityCoordinates.ts" rename to "v\303\246r/src/hooks/useGetCityCoordinates.ts" From 8e05d97314c338163777f5933fbbbfbf97972bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Tue, 15 Sep 2026 18:59:33 +0200 Subject: [PATCH 3/3] Validate altitudes in weather search --- "v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git "a/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" "b/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" index eefd174..3ff66b6 100644 --- "a/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" +++ "b/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts" @@ -2,9 +2,15 @@ import { useQuery } from "@tanstack/react-query" import { type WeatherApiResponse } from "./types" const useGetWeather = (lat: number, lon: number, alt?: number) => { + let sanitized_alt = alt + if (alt != undefined) { + sanitized_alt = alt > 9000 ? 9000 : alt + sanitized_alt = sanitized_alt < -500 ? -500 : sanitized_alt + } + const { isLoading, error, data } = useQuery({ queryKey: ["citySearch", `${lat}-${lon}`], - queryFn: () => fetch(`https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${lat}&lon=${lon}${alt ? `&altitude=${alt}` : ""}`).then((res) => res.json() as Promise) + queryFn: () => fetch(`https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${lat}&lon=${lon}${alt != undefined ? `&altitude=${sanitized_alt}` : ""}`).then((res) => res.json() as Promise) }) return { isLoading, error, result: data }