diff --git a/.github/workflows/deploy-to-vm.yaml b/.github/workflows/deploy-to-vm.yaml
index 10aa960..8ae18d7 100644
--- a/.github/workflows/deploy-to-vm.yaml
+++ b/.github/workflows/deploy-to-vm.yaml
@@ -34,13 +34,11 @@ jobs:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Install dependencies
- working-directory: vær
run: |
mkdir -p "$TMPDIR"
npm ci --no-audit --no-fund
- name: Build app
- working-directory: vær
run: npm run build
- name: Deploy app to Apache
diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/lint-and-format.yaml
index b8d2c3c..1a36c18 100644
--- a/.github/workflows/lint-and-format.yaml
+++ b/.github/workflows/lint-and-format.yaml
@@ -7,9 +7,10 @@ on:
pull_request:
types:
- opened
+ - synchronize
jobs:
- deploy:
+ lint-and-format:
runs-on: [self-hosted]
timeout-minutes: 10
@@ -23,15 +24,12 @@ jobs:
uses: actions/checkout@v4
- name: Install dependencies
- working-directory: vær
run: |
mkdir -p "$TMPDIR"
npm ci --no-audit --no-fund
- name: Run Prettier check
- working-directory: vær
- run: npx exec prettier . --check
+ run: npx prettier . --check
- name: Run ESLint
- working-directory: vær
run: npm run lint
diff --git "a/v\303\246r/.prettierrc" b/.prettierrc
similarity index 100%
rename from "v\303\246r/.prettierrc"
rename to .prettierrc
diff --git a/README.md b/README.md
index 6c31c05..813ba3e 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,73 @@
-# T09-Project-1
+# React + TypeScript + Vite
-Repository for team IT2810-H26-T09 in IT2810-H26.
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-## Working in this repository
+Currently, two official plugins are available:
-`main` is protected — you cannot push to it directly. Work on a branch and
-open a pull request:
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
-```bash
-git checkout -b my-feature
-git push -u origin my-feature
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(["dist"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ["./tsconfig.node.json", "./tsconfig.app.json"],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
```
-Before a pull request can be merged:
+You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules:
-1. **A member of IT2810-H26-T09 must approve it.** You cannot approve your own pull
- request, and approvals from staff do not count — you review each other's
- code.
-2. **Every review comment must be marked resolved.** Reply to each one, fix it
- or explain why not, then click *Resolve conversation*.
-3. **Pushing new commits dismisses the approval**, so if you change something
- after being approved, ask for another review. This is deliberate: what was
- approved is what gets merged.
\ No newline at end of file
+```js
+// eslint.config.js
+import reactX from "eslint-plugin-react-x"
+import reactDom from "eslint-plugin-react-dom"
+
+export default defineConfig([
+ globalIgnores(["dist"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs["recommended-typescript"],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ["./tsconfig.node.json", "./tsconfig.app.json"],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..9934c1c
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,22 @@
+import js from "@eslint/js"
+import globals from "globals"
+import reactHooks from "eslint-plugin-react-hooks"
+import reactRefresh from "eslint-plugin-react-refresh"
+import tseslint from "typescript-eslint"
+import { defineConfig, globalIgnores } from "eslint/config"
+
+export default defineConfig([
+ globalIgnores(["dist"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git "a/v\303\246r/index.html" b/index.html
similarity index 100%
rename from "v\303\246r/index.html"
rename to index.html
diff --git "a/v\303\246r/package-lock.json" b/package-lock.json
similarity index 100%
rename from "v\303\246r/package-lock.json"
rename to package-lock.json
diff --git "a/v\303\246r/package.json" b/package.json
similarity index 100%
rename from "v\303\246r/package.json"
rename to package.json
diff --git "a/v\303\246r/public/favicon.svg" b/public/favicon.svg
similarity index 100%
rename from "v\303\246r/public/favicon.svg"
rename to public/favicon.svg
diff --git "a/v\303\246r/public/icons.svg" b/public/icons.svg
similarity index 100%
rename from "v\303\246r/public/icons.svg"
rename to public/icons.svg
diff --git "a/v\303\246r/src/App.css" b/src/App.css
similarity index 99%
rename from "v\303\246r/src/App.css"
rename to src/App.css
index f90339d..f460279 100644
--- "a/v\303\246r/src/App.css"
+++ b/src/App.css
@@ -167,7 +167,7 @@
&::before,
&::after {
- content: '';
+ content: "";
position: absolute;
top: -4.5px;
border: 5px solid transparent;
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..a1fc0a6
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,17 @@
+import "./App.css"
+import LocationList from "./components/location-list"
+import ViewCity from "./components/ViewCity"
+
+function App() {
+ return (
+ <>
+
+
+ >
+ )
+}
+
+export default App
diff --git "a/v\303\246r/src/assets/hero.png" b/src/assets/hero.png
similarity index 100%
rename from "v\303\246r/src/assets/hero.png"
rename to src/assets/hero.png
diff --git "a/v\303\246r/src/assets/react.svg" b/src/assets/react.svg
similarity index 100%
rename from "v\303\246r/src/assets/react.svg"
rename to src/assets/react.svg
diff --git "a/v\303\246r/src/assets/sivertbbdahls.png" b/src/assets/sivertbbdahls.png
similarity index 100%
rename from "v\303\246r/src/assets/sivertbbdahls.png"
rename to src/assets/sivertbbdahls.png
diff --git "a/v\303\246r/src/assets/vite.svg" b/src/assets/vite.svg
similarity index 100%
rename from "v\303\246r/src/assets/vite.svg"
rename to src/assets/vite.svg
diff --git a/src/components/ViewCity.tsx b/src/components/ViewCity.tsx
new file mode 100644
index 0000000..f11f128
--- /dev/null
+++ b/src/components/ViewCity.tsx
@@ -0,0 +1,33 @@
+import useGetWeather from "../hooks/fetchWeather/useGetWeather"
+import useGetCityCoordinates from "../hooks/useGetCityCoordinates"
+
+interface ViewCityProp {
+ city: string
+}
+
+export default function ViewCity(arg: ViewCityProp) {
+ const data = useGetCityCoordinates(arg.city)
+
+ const { isLoading, error, result } = useGetWeather(
+ data.result[0]?.latitude,
+ data.result[0]?.longitude
+ )
+
+ if (isLoading) {
+ return
Laster
+ }
+ if (error) {
+ return Det skjedde en feil
+ }
+ const firstTimeStep = result?.properties.timeseries[0]
+ const windSpeed = firstTimeStep?.data.instant.details?.wind_speed
+ const temperature = firstTimeStep?.data.instant.details?.air_temperature
+
+ return (
+
+
Været i {arg.city}
+
Temperatur: {temperature}
+
Vindhastighet: {windSpeed}
+
+ )
+}
diff --git a/src/components/listItem.tsx b/src/components/listItem.tsx
new file mode 100644
index 0000000..b55799b
--- /dev/null
+++ b/src/components/listItem.tsx
@@ -0,0 +1,51 @@
+type cityWeather = {
+ name: string
+ temperature?: number
+ precipitation?: number
+ nextDays?: { time: string; temperature: number | undefined }[]
+}
+
+const listItem = ({ city: item }: { city: cityWeather }) => {
+ return (
+
+
+ {item.name}:
+
+ {typeof item.precipitation === "number" && item.precipitation > 0.6
+ ? "🌧️"
+ : "⛅"}
+
+
+ {typeof item.temperature === "number"
+ ? `${item.temperature.toFixed()}°`
+ : "—"}
+
+
+
+
+ {item.nextDays?.map((day) => (
+
+ {typeof day.temperature === "number"
+ ? `${day.temperature.toFixed()}°`
+ : "—"}
+
+ ))}
+
+
+ )
+}
+
+export default listItem
diff --git a/src/components/location-list.tsx b/src/components/location-list.tsx
new file mode 100644
index 0000000..9476553
--- /dev/null
+++ b/src/components/location-list.tsx
@@ -0,0 +1,111 @@
+import type { WeatherApiResponse } from "../hooks/fetchWeather/types"
+import useGetWeather from "../hooks/fetchWeather/useGetWeather"
+import ListItem from "./listItem"
+
+type City = {
+ name: string
+ lat: number
+ lon: number
+}
+
+const cities: City[] = [
+ { name: "Oslo", lat: 59.91273, lon: 10.74609 },
+ { name: "Sørumsand", lat: 59.98621, lon: 11.24154 },
+ { name: "Trondheim", lat: 63.41667, lon: 10.41667 },
+ { name: "Las Vegas", lat: 36.1699, lon: -115.1398 },
+]
+
+const getTemperature = (data?: WeatherApiResponse) =>
+ data?.properties?.timeseries?.[0]?.data?.instant?.details?.air_temperature
+
+const getPrecipitation = (data?: WeatherApiResponse) =>
+ data?.properties?.timeseries?.[0]?.data?.next_6_hours?.details
+ ?.precipitation_amount
+
+const getNextDays = (data?: WeatherApiResponse) => {
+ const now = new Date()
+
+ const noonTemperatures = data?.properties.timeseries
+ .filter((entry) => {
+ const date = new Date(entry.time)
+ return (
+ date >= now &&
+ date.getUTCHours() === 12 &&
+ date < new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000)
+ )
+ })
+ .map((entry) => ({
+ time: entry.time,
+ temperature: entry.data.instant.details?.air_temperature,
+ }))
+
+ return noonTemperatures
+}
+
+const LocationList = () => {
+ const weatherResults = cities.map((city) => useGetWeather(city.lat, city.lon))
+
+ console.log(weatherResults)
+
+ if (weatherResults.some((result) => result.isLoading)) {
+ return Loading weather…
+ }
+
+ if (weatherResults.some((result) => result.error)) {
+ return Could not load weather data.
+ }
+
+ const cityWeather = cities.map((city, index) => ({
+ name: city.name,
+ temperature: getTemperature(weatherResults[index]?.result),
+ precipitation: getPrecipitation(weatherResults[index]?.result),
+ nextDays: getNextDays(weatherResults[index]?.result),
+ }))
+
+ return (
+
+
+ City
+ Next 3 Days
+
+
+
+
+ {cityWeather.map((item) => (
+
+ // -
+ // {item.name}: {typeof item.temperature === 'number' ? `${item.temperature}°` : '—'}
+ // {typeof item.precipitation === 'number' ? `, ${item.precipitation} mm` : ''}
+ //
+ ))}
+
+
+ )
+}
+
+export default LocationList
diff --git a/src/hooks/fetchWeather/types.ts b/src/hooks/fetchWeather/types.ts
new file mode 100644
index 0000000..1c008ba
--- /dev/null
+++ b/src/hooks/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/src/hooks/fetchWeather/useGetWeather.ts b/src/hooks/fetchWeather/useGetWeather.ts
new file mode 100644
index 0000000..163650e
--- /dev/null
+++ b/src/hooks/fetchWeather/useGetWeather.ts
@@ -0,0 +1,22 @@
+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 != undefined ? `&altitude=${sanitized_alt}` : ""}`
+ ).then((res) => res.json() as Promise),
+ })
+
+ return { isLoading, error, result: data }
+}
+
+export default useGetWeather
diff --git a/src/hooks/useGetCityCoordinates.ts b/src/hooks/useGetCityCoordinates.ts
new file mode 100644
index 0000000..ffa0b74
--- /dev/null
+++ b/src/hooks/useGetCityCoordinates.ts
@@ -0,0 +1,27 @@
+import { useQuery } from "@tanstack/react-query"
+
+export type citySearchResult = {
+ results: {
+ 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?.results ?? []
+
+ return { isLoading, error, result }
+}
+
+export default useGetCityCoordinates
diff --git a/src/hooks/useStorage.ts b/src/hooks/useStorage.ts
new file mode 100644
index 0000000..450782f
--- /dev/null
+++ b/src/hooks/useStorage.ts
@@ -0,0 +1,51 @@
+import { useState, useCallback } from "react"
+import { getData, saveData } from "../services/storage"
+import type { AppStorage } from "../services/storage"
+
+export function useAppStorage() {
+ const [storage, setStorage] = useState(getData)
+
+ const updateStorage = useCallback(
+ (updater: Partial | ((prev: AppStorage) => AppStorage)) => {
+ setStorage((prev) => {
+ const nextState =
+ typeof updater === "function"
+ ? updater(prev)
+ : { ...prev, ...updater }
+ saveData(nextState)
+ return nextState
+ })
+ },
+ []
+ )
+
+ // Star / unstar a location easily
+ const toggleStarredLocation = useCallback(
+ (locationId: string) => {
+ updateStorage((prev) => {
+ const exists = prev.starredLocations.includes(locationId)
+ const starredLocations = exists
+ ? prev.starredLocations.filter((id) => id !== locationId)
+ : [...prev.starredLocations, locationId]
+
+ return { ...prev, starredLocations }
+ })
+ },
+ [updateStorage]
+ )
+
+ // Update last visited page
+ const setLastPage = useCallback(
+ (lastPage: string) => {
+ updateStorage({ lastPage })
+ },
+ [updateStorage]
+ )
+
+ return {
+ storage,
+ updateStorage,
+ toggleStarredLocation,
+ setLastPage,
+ }
+}
diff --git "a/v\303\246r/src/index.css" b/src/index.css
similarity index 90%
rename from "v\303\246r/src/index.css"
rename to src/index.css
index 094bb6c..d028fa2 100644
--- "a/v\303\246r/src/index.css"
+++ b/src/index.css
@@ -11,8 +11,8 @@
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
- --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
- --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --sans: system-ui, "Segoe UI", Roboto, sans-serif;
+ --heading: system-ui, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
@@ -92,5 +92,5 @@ code {
ul {
list-style-type: none; /* Removes the dots */
- padding: 0; /* Removes default left indentation */
-}
\ No newline at end of file
+ padding: 0; /* Removes default left indentation */
+}
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..09e3a63
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,15 @@
+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(
+
+
+
+
+
+)
diff --git a/src/services/storage.ts b/src/services/storage.ts
new file mode 100644
index 0000000..1ac8d22
--- /dev/null
+++ b/src/services/storage.ts
@@ -0,0 +1,46 @@
+const STORAGE_KEY = "vær:storage_key"
+const STORAGE_VERSION = 1
+
+//Type for tilstand
+export interface AppStorage {
+ version: number
+ starredLocations: string[]
+ lastPage: string
+}
+
+const DEFAULT_STATE: AppStorage = {
+ version: STORAGE_VERSION,
+ starredLocations: [],
+ lastPage: "home",
+}
+
+export function getData(): AppStorage {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY)
+ if (!raw) return DEFAULT_STATE
+
+ const parsed = JSON.parse(raw) as AppStorage
+
+ if (parsed.version !== STORAGE_VERSION) return DEFAULT_STATE
+
+ return { ...DEFAULT_STATE, ...parsed }
+ } catch {
+ console.error("Failed to load store data")
+ return DEFAULT_STATE
+ }
+}
+
+export function saveData(state: AppStorage): void {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
+ } catch (err) {
+ console.error("Failed saving", err)
+ }
+}
+
+// export function updateAppStorage(patch: Partial): AppStorage {
+// const current = getData();
+// const updated = { ...current, ...patch };
+// saveData(updated);
+// return updated;
+// }
diff --git "a/v\303\246r/tsconfig.app.json" b/tsconfig.app.json
similarity index 100%
rename from "v\303\246r/tsconfig.app.json"
rename to tsconfig.app.json
diff --git "a/v\303\246r/tsconfig.json" b/tsconfig.json
similarity index 100%
rename from "v\303\246r/tsconfig.json"
rename to tsconfig.json
diff --git "a/v\303\246r/tsconfig.node.json" b/tsconfig.node.json
similarity index 100%
rename from "v\303\246r/tsconfig.node.json"
rename to tsconfig.node.json
diff --git "a/v\303\246r/vite.config.ts" b/vite.config.ts
similarity index 57%
rename from "v\303\246r/vite.config.ts"
rename to vite.config.ts
index fc089ba..5eb9443 100644
--- "a/v\303\246r/vite.config.ts"
+++ b/vite.config.ts
@@ -1,5 +1,5 @@
-import react from '@vitejs/plugin-react'
-import { defineConfig } from 'vite'
+import react from "@vitejs/plugin-react"
+import { defineConfig } from "vite"
// https://vite.dev/config/
export default defineConfig({
diff --git "a/v\303\246r/README.md" "b/v\303\246r/README.md"
deleted file mode 100644
index 2d31cb6..0000000
--- "a/v\303\246r/README.md"
+++ /dev/null
@@ -1,75 +0,0 @@
-# React + TypeScript + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
-
-## React Compiler
-
-The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
-
-```js
-export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{ts,tsx}'],
- extends: [
- // Other configs...
-
- // Remove tseslint.configs.recommended and replace with this
- tseslint.configs.recommendedTypeChecked,
- // Alternatively, use this for stricter rules
- tseslint.configs.strictTypeChecked,
- // Optionally, add this for stylistic rules
- tseslint.configs.stylisticTypeChecked,
-
- // Other configs...
- ],
- languageOptions: {
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- // other options...
- },
- },
-])
-
-```
-
-You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules:
-
-```js
-// eslint.config.js
-import reactX from 'eslint-plugin-react-x'
-import reactDom from 'eslint-plugin-react-dom'
-
-export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{ts,tsx}'],
- extends: [
- // Other configs...
- // Enable lint rules for React
- reactX.configs['recommended-typescript'],
- // Enable lint rules for React DOM
- reactDom.configs.recommended,
- ],
- languageOptions: {
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- // other options...
- },
- },
-])
-
-```
diff --git "a/v\303\246r/eslint.config.js" "b/v\303\246r/eslint.config.js"
deleted file mode 100644
index ef614d2..0000000
--- "a/v\303\246r/eslint.config.js"
+++ /dev/null
@@ -1,22 +0,0 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
-import tseslint from 'typescript-eslint'
-import { defineConfig, globalIgnores } from 'eslint/config'
-
-export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{ts,tsx}'],
- extends: [
- js.configs.recommended,
- tseslint.configs.recommended,
- reactHooks.configs.flat.recommended,
- reactRefresh.configs.vite,
- ],
- languageOptions: {
- globals: globals.browser,
- },
- },
-])
diff --git "a/v\303\246r/src/App.tsx" "b/v\303\246r/src/App.tsx"
deleted file mode 100644
index 6f346a9..0000000
--- "a/v\303\246r/src/App.tsx"
+++ /dev/null
@@ -1,19 +0,0 @@
-import './App.css'
-import LocationList from './components/location-list'
-import ViewCity from './components/ViewCity'
-import useGetWeather from './hooks/fetchWeather/useGetWeather'
-
-function App() {
-
- return (
- <>
-
-
- >
- )
-}
-
-export default App
diff --git "a/v\303\246r/src/components/ViewCity.tsx" "b/v\303\246r/src/components/ViewCity.tsx"
deleted file mode 100644
index 7a2c13a..0000000
--- "a/v\303\246r/src/components/ViewCity.tsx"
+++ /dev/null
@@ -1,32 +0,0 @@
-import useGetWeather from "../hooks/fetchWeather/useGetWeather";
-import useGetCityCoordinates from "../hooks/useGetCityCoordinates";
-
-interface ViewCityProp {
- city: string
-}
-
-export default function ViewCity(arg: ViewCityProp){
-
- const data = useGetCityCoordinates(arg.city);
-
- const {isLoading, error, result} = useGetWeather(data.result[0]?.latitude, data.result[0]?.longitude);
-
- if(isLoading){
- return Laster
;
- }
- if(error){
-
- return Det skjedde en feil
- }
- const firstTimeStep = result?.properties.timeseries[0];
- const windSpeed = firstTimeStep?.data.instant.details?.wind_speed;
- const temperature = firstTimeStep?.data.instant.details?.air_temperature;
-
- return (
-
-
Været i {arg.city}
-
Temperatur: {temperature}
-
Vindhastighet: {windSpeed}
-
- );
-}
\ No newline at end of file
diff --git "a/v\303\246r/src/components/listItem.tsx" "b/v\303\246r/src/components/listItem.tsx"
deleted file mode 100644
index 91f3436..0000000
--- "a/v\303\246r/src/components/listItem.tsx"
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-type cityWeather = {
- name: string
- temperature?: number
- precipitation?: number
- nextDays?: { time: string; temperature: number | undefined}[]
-}
-
-
-const listItem = ({ city: item }: { city: cityWeather }) => {
-
- return (
-
-
- {item.name}:
-
- {typeof item.precipitation === 'number' && item.precipitation > 0.6 ? '🌧️' : '⛅'}
-
- {typeof item.temperature === 'number' ? `${item.temperature.toFixed()}°` : '—'}
-
-
-
-
- {item.nextDays?.map((day) => (
-
- {typeof day.temperature === 'number' ? `${day.temperature.toFixed()}°` : '—'}
-
- ))}
-
-
-
- )
-}
-
-export default listItem
diff --git "a/v\303\246r/src/components/location-list.tsx" "b/v\303\246r/src/components/location-list.tsx"
deleted file mode 100644
index d28f8ff..0000000
--- "a/v\303\246r/src/components/location-list.tsx"
+++ /dev/null
@@ -1,82 +0,0 @@
-import type { WeatherApiResponse } from '../hooks/fetchWeather/types'
-import useGetWeather from '../hooks/fetchWeather/useGetWeather'
-import ListItem from './listItem'
-
-type City = {
- name: string
- lat: number
- lon: number
-}
-
-const cities: City[] = [
- { name: 'Oslo', lat: 59.91273, lon: 10.74609 },
- { name: 'Sørumsand', lat: 59.98621, lon: 11.24154 },
- { name: 'Trondheim', lat: 63.41667, lon: 10.41667 },
- { name: 'Las Vegas', lat: 36.1699, lon: -115.1398 },
-]
-
-const getTemperature = (data?: WeatherApiResponse) =>
- data?.properties?.timeseries?.[0]?.data?.instant?.details?.air_temperature
-
-const getPrecipitation = (data?: WeatherApiResponse) =>
- data?.properties?.timeseries?.[0]?.data?.next_6_hours?.details?.precipitation_amount
-
-const getNextDays = (data?: WeatherApiResponse) => {
- const now = new Date()
-
- const noonTemperatures = data?.properties.timeseries
- .filter((entry) => {
- const date = new Date(entry.time)
- return date >= now && date.getUTCHours() === 12 && date < new Date(now.getTime() + 3* 24 * 60 * 60 * 1000)
- })
- .map((entry) => ({
- time: entry.time,
- temperature: entry.data.instant.details?.air_temperature,
- }))
-
- return noonTemperatures
-}
-
-
-const LocationList = () => {
- const weatherResults = cities.map((city) => useGetWeather(city.lat, city.lon))
-
- console.log(weatherResults)
-
- if (weatherResults.some((result) => result.isLoading)) {
- return Loading weather…
- }
-
- if (weatherResults.some((result) => result.error)) {
- return Could not load weather data.
- }
-
- const cityWeather = cities.map((city, index) => ({
- name: city.name,
- temperature: getTemperature(weatherResults[index]?.result),
- precipitation: getPrecipitation(weatherResults[index]?.result),
- nextDays: getNextDays(weatherResults[index]?.result),
- }))
-
- return (
-
-
- City
- Next 3 Days
-
-
-
-
- {cityWeather.map((item) => (
-
- // -
- // {item.name}: {typeof item.temperature === 'number' ? `${item.temperature}°` : '—'}
- // {typeof item.precipitation === 'number' ? `, ${item.precipitation} mm` : ''}
- //
- ))}
-
-
- )
-}
-
-export default LocationList
diff --git "a/v\303\246r/src/hooks/fetchWeather/types.ts" "b/v\303\246r/src/hooks/fetchWeather/types.ts"
deleted file mode 100644
index 702841c..0000000
--- "a/v\303\246r/src/hooks/fetchWeather/types.ts"
+++ /dev/null
@@ -1,178 +0,0 @@
-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/hooks/fetchWeather/useGetWeather.ts" "b/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts"
deleted file mode 100644
index 3ff66b6..0000000
--- "a/v\303\246r/src/hooks/fetchWeather/useGetWeather.ts"
+++ /dev/null
@@ -1,19 +0,0 @@
-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 != undefined ? `&altitude=${sanitized_alt}` : ""}`).then((res) => res.json() as Promise)
- })
-
- return { isLoading, error, result: data }
-}
-
-export default useGetWeather
diff --git "a/v\303\246r/src/hooks/useGetCityCoordinates.ts" "b/v\303\246r/src/hooks/useGetCityCoordinates.ts"
deleted file mode 100644
index 7397f5f..0000000
--- "a/v\303\246r/src/hooks/useGetCityCoordinates.ts"
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useQuery } from "@tanstack/react-query"
-
-export type citySearchResult = {
- results: {
- 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?.results ?? []
-
- return { isLoading, error, result }
-}
-
-export default useGetCityCoordinates
diff --git "a/v\303\246r/src/hooks/useStorage.ts" "b/v\303\246r/src/hooks/useStorage.ts"
deleted file mode 100644
index 589e460..0000000
--- "a/v\303\246r/src/hooks/useStorage.ts"
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useState, useCallback } from "react";
-import { getData, saveData } from "../services/storage";
-import type {AppStorage} from "../services/storage";
-
-export function useAppStorage() {
-
- const [storage, setStorage] = useState(getData);
-
- const updateStorage = useCallback((updater: Partial | ((prev: AppStorage) => AppStorage)) => {
- setStorage((prev) => {
- const nextState = typeof updater === "function" ? updater(prev) : { ...prev, ...updater };
- saveData(nextState);
- return nextState;
- });
- }, []);
-
- // Star / unstar a location easily
- const toggleStarredLocation = useCallback((locationId: string) => {
- updateStorage((prev) => {
- const exists = prev.starredLocations.includes(locationId);
- const starredLocations = exists
- ? prev.starredLocations.filter((id) => id !== locationId)
- : [...prev.starredLocations, locationId];
-
- return { ...prev, starredLocations };
- });
- }, [updateStorage]);
-
- // Update last visited page
- const setLastPage = useCallback((lastPage: string) => {
- updateStorage({ lastPage });
- }, [updateStorage]);
-
- return {
- storage,
- updateStorage,
- toggleStarredLocation,
- setLastPage,
- };
-}
\ No newline at end of file
diff --git "a/v\303\246r/src/main.tsx" "b/v\303\246r/src/main.tsx"
deleted file mode 100644
index accea2b..0000000
--- "a/v\303\246r/src/main.tsx"
+++ /dev/null
@@ -1,18 +0,0 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-import ViewCity from './components/ViewCity.tsx'
-import {
- QueryClient, QueryClientProvider
-} from '@tanstack/react-query'
-
-const queryClient = new QueryClient()
-
-createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
diff --git "a/v\303\246r/src/services/storage.ts" "b/v\303\246r/src/services/storage.ts"
deleted file mode 100644
index 2a8f0b9..0000000
--- "a/v\303\246r/src/services/storage.ts"
+++ /dev/null
@@ -1,48 +0,0 @@
-const STORAGE_KEY = "vær:storage_key"
-const STORAGE_VERSION = 1;
-
-//Type for tilstand
-export interface AppStorage{
- version: number;
- starredLocations: string[]
- lastPage: string;
-}
-
-const DEFAULT_STATE: AppStorage = {
- version: STORAGE_VERSION,
- starredLocations: [],
- lastPage: "home",
-};
-
-export function getData(): AppStorage{
- try{
- const raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) return DEFAULT_STATE;
-
- const parsed = JSON.parse(raw) as AppStorage;
-
- if (parsed.version !== STORAGE_VERSION) return DEFAULT_STATE;
-
- return {...DEFAULT_STATE, ...parsed};
- }
- catch (err){
- console.error("Failed to load store data");
- return DEFAULT_STATE;
- }
-}
-
-export function saveData(state: AppStorage): void{
- try{
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
-
- }catch(err){
- console.error("Failed saving", err);
- }
-}
-
-// export function updateAppStorage(patch: Partial): AppStorage {
-// const current = getData();
-// const updated = { ...current, ...patch };
-// saveData(updated);
-// return updated;
-// }
\ No newline at end of file