diff --git a/package.json b/package.json index 5fc0534..e0b0da8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "lint-fix": "eslint . --fix", "preview": "vite preview", "fmt": "npx prettier . --write", "test": "vitest run", diff --git a/src/App.tsx b/src/App.tsx index 27b10c2..fd4b486 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,19 +3,24 @@ import "./App.css" import LocationList from "./components/location-list" import ViewCity from "./components/ViewCity" import SearchBar from "./components/SearchBar" +import { useAppStorage } from "./hooks/useStorage" function App() { const [selectedCity, setSelectedCity] = useState("Sørumsand") const handleSearch = (query: string) => { setSelectedCity(query) } + const storageHook = useAppStorage() return ( <>
- + - +
diff --git a/src/components/ViewCity.tsx b/src/components/ViewCity.tsx index c9d59b3..8f4c7aa 100644 --- a/src/components/ViewCity.tsx +++ b/src/components/ViewCity.tsx @@ -1,13 +1,19 @@ import useGetWeather from "../hooks/fetchWeather/useGetWeather" import useGetCityCoordinates from "../hooks/fetchCities/useGetCityCoordinates" import StarButton from "./starButton" +import type { AppStorage } from "../services/storage" +import type { City } from "../hooks/fetchWeather/types" interface ViewCityProp { - city: string + cityName: string + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } } export default function ViewCity(arg: ViewCityProp) { - const data = useGetCityCoordinates({ cityName: arg.city }) + const data = useGetCityCoordinates({ cityName: arg.cityName }) const { isLoading, error, result } = useGetWeather( data.result[0]?.latitude, @@ -101,7 +107,7 @@ export default function ViewCity(arg: ViewCityProp) { fontWeight: "500", }} > - {arg.city} + {arg.cityName}

- +

{ +const listItem = ({ + city, + storageHook, +}: { + city: cityWeather + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } +}) => { return ( -
  • +
  • { alignItems: "center", }} > - - {item.name}: + + {city.name}: - {typeof item.precipitation === "number" && item.precipitation > 0.6 + {typeof city.precipitation === "number" && city.precipitation > 0.6 ? "🌧️" : "⛅"}

    - {typeof item.temperature === "number" - ? `${item.temperature.toFixed()}°` + {typeof city.temperature === "number" + ? `${city.temperature.toFixed()}°` : "—"}

    @@ -41,7 +54,7 @@ const listItem = ({ city: item }: { city: cityWeather }) => { alignSelf: "end", }} > - {item.nextDays?.map((day) => ( + {city.nextDays?.map((day) => ( {typeof day.precipitation === "number" && day.precipitation > 0.6 ? "🌧️" diff --git a/src/components/location-list.tsx b/src/components/location-list.tsx index b249aa9..e6640bd 100644 --- a/src/components/location-list.tsx +++ b/src/components/location-list.tsx @@ -1,10 +1,11 @@ import type { WeatherApiResponse, City } from "../hooks/fetchWeather/types" import { useGetWeatherForCities } from "../hooks/fetchWeather/useGetWeather" import ListItem from "./listItem" +import type { AppStorage } from "../services/storage" const cities: City[] = [ - { name: "Oslo", lat: 59.91273, lon: 10.74609 }, { name: "Sørumsand", lat: 59.98621, lon: 11.24154 }, + { name: "Oslo", lat: 59.91273, lon: 10.74609 }, { name: "Trondheim", lat: 63.41667, lon: 10.41667 }, { name: "Las Vegas", lat: 36.1699, lon: -115.1398 }, ] @@ -25,12 +26,7 @@ const getNextDays = (data?: WeatherApiResponse) => { return ( date >= now && date.getUTCHours() === 12 && - date < - new Date( - now.getTime() - - now.getHours() * 60 * 60 * 1000 + - 4 * 24 * 60 * 60 * 1000 - ) + date < new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000) ) }) .map((entry) => ({ @@ -44,17 +40,36 @@ const getNextDays = (data?: WeatherApiResponse) => { interface LocationListProps { onCitySelect: (cityName: string) => void + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } } -const LocationList = ({ onCitySelect }: LocationListProps) => { +const LocationList = ({ onCitySelect, storageHook }: LocationListProps) => { + if (storageHook) { + if (storageHook.storage.starredLocations.length > 0) { + if (storageHook.storage.starredLocations.length >= 4) { + cities.splice(0, 4) + } + for (const starredLocation of storageHook.storage.starredLocations) { + if (!cities.some((city) => city.name === starredLocation.name)) { + cities.unshift(starredLocation) + } + } + } + } + const { isLoading, results } = useGetWeatherForCities(cities) if (isLoading) { - return
    Loading...
    + return
    Laster...
    } const cityWeather = cities.map((city, index) => ({ name: city.name, + lat: city.lat, + lon: city.lon, temperature: getTemperature(results[index]), precipitation: getPrecipitation(results[index]), nextDays: getNextDays(results[index]), @@ -99,7 +114,7 @@ const LocationList = ({ onCitySelect }: LocationListProps) => { key={item.name} style={{ width: "80%" }} > - +
  • ))} diff --git a/src/components/starButton.tsx b/src/components/starButton.tsx index 7831590..83a0408 100644 --- a/src/components/starButton.tsx +++ b/src/components/starButton.tsx @@ -1,18 +1,30 @@ -import { useAppStorage } from "../hooks/useStorage" +import type { City } from "../hooks/fetchWeather/types" +import type { AppStorage } from "../services/storage" -const StarButton = ({ cityName }: { cityName: string }) => { - const { storage, toggleStarredLocation } = useAppStorage() +const StarButton = ({ + city, + storageHook, +}: { + city: City + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } +}) => { + const isStarred = storageHook?.storage.starredLocations.some( + (starredCity) => starredCity.name === city.name + ) const handleClick = () => { - toggleStarredLocation(cityName) + storageHook?.toggleStarredLocation(city) } return ( ) diff --git a/src/hooks/useStorage.ts b/src/hooks/useStorage.ts index 450782f..4725873 100644 --- a/src/hooks/useStorage.ts +++ b/src/hooks/useStorage.ts @@ -1,6 +1,7 @@ import { useState, useCallback } from "react" import { getData, saveData } from "../services/storage" import type { AppStorage } from "../services/storage" +import type { City } from "./fetchWeather/types" export function useAppStorage() { const [storage, setStorage] = useState(getData) @@ -21,12 +22,14 @@ export function useAppStorage() { // Star / unstar a location easily const toggleStarredLocation = useCallback( - (locationId: string) => { + (location: City) => { updateStorage((prev) => { - const exists = prev.starredLocations.includes(locationId) + const exists = prev.starredLocations.some( + (loc) => loc.name === location.name + ) const starredLocations = exists - ? prev.starredLocations.filter((id) => id !== locationId) - : [...prev.starredLocations, locationId] + ? prev.starredLocations.filter((loc) => loc.name !== location.name) + : [...prev.starredLocations, location] return { ...prev, starredLocations } }) diff --git a/src/index.css b/src/index.css index 8d5f416..05689fc 100644 --- a/src/index.css +++ b/src/index.css @@ -96,6 +96,7 @@ ul { } button { + color: #000; border: none; background: transparent; cursor: pointer; diff --git a/src/services/storage.ts b/src/services/storage.ts index 1ac8d22..99f62e5 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -1,10 +1,12 @@ +import type { City } from "../hooks/fetchWeather/types" + const STORAGE_KEY = "vær:storage_key" const STORAGE_VERSION = 1 //Type for tilstand export interface AppStorage { version: number - starredLocations: string[] + starredLocations: City[] lastPage: string } diff --git a/tests/components/ViewCity.test.tsx b/tests/components/ViewCity.test.tsx index 4937ef3..442c694 100644 --- a/tests/components/ViewCity.test.tsx +++ b/tests/components/ViewCity.test.tsx @@ -51,7 +51,7 @@ describe("ViewCity", () => { result: [{ latitude: 59.9, longitude: 10.7, country: "Norway" }], }) - render() + render() expect(screen.getByText("Oslo")).toBeTruthy() expect(screen.getByText("Norway")).toBeTruthy() @@ -68,7 +68,7 @@ describe("ViewCity", () => { it("should render a loading message while weather data is loading", () => { mockUseGetWeather.mockReturnValue({ isLoading: true, error: null }) - render() + render() expect(screen.getByText("Laster...")).toBeTruthy() }) @@ -79,7 +79,7 @@ describe("ViewCity", () => { error: new Error("Request failed"), }) - render() + render() expect(screen.getByText("Det skjedde en feil")).toBeTruthy() }) diff --git a/tests/components/listItem.test.tsx b/tests/components/listItem.test.tsx index 0fe4d7f..4369e9f 100644 --- a/tests/components/listItem.test.tsx +++ b/tests/components/listItem.test.tsx @@ -10,6 +10,8 @@ describe("ListItem", () => { { }) it("should show fallback values when weather data is unavailable", () => { - render() + render( + + ) expect(screen.getByText("⛅")).toBeTruthy() expect(screen.getByText("—")).toBeTruthy() diff --git a/tests/components/location-list.test.tsx b/tests/components/location-list.test.tsx index adb9d99..d909f04 100644 --- a/tests/components/location-list.test.tsx +++ b/tests/components/location-list.test.tsx @@ -36,7 +36,7 @@ describe("LocationList", () => { render( undefined} />) - expect(screen.getByText("Loading...")).toBeTruthy() + expect(screen.getByText("Laster...")).toBeTruthy() }) it("should render a weather row for every configured city", () => {