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 +
    +
    + + +
    + ) +} + +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