From fad38629b845e1995941ee12e510553974d10e38 Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 18 Sep 2026 01:00:55 +0200 Subject: [PATCH 1/6] pin favorites --- src/components/location-list.tsx | 19 ++++++++++++++----- src/components/starButton.tsx | 4 ++-- src/index.css | 1 + 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/location-list.tsx b/src/components/location-list.tsx index b249aa9..7112473 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 { useAppStorage} from "../hooks/useStorage" 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 }, ] @@ -27,9 +28,8 @@ const getNextDays = (data?: WeatherApiResponse) => { date.getUTCHours() === 12 && date < new Date( - now.getTime() - - now.getHours() * 60 * 60 * 1000 + - 4 * 24 * 60 * 60 * 1000 + now.getTime() + + 3 * 24 * 60 * 60 * 1000 ) ) }) @@ -47,10 +47,19 @@ interface LocationListProps { } const LocationList = ({ onCitySelect }: LocationListProps) => { + const { storage } = useAppStorage() + + if (storage.starredLocations.length > 0) { + for (const starredLocation of storage.starredLocations) { + if (!cities.some((city) => city.name === starredLocation)) { + cities.unshift({ name: starredLocation, lat: 0, lon: 0}) + } + }} + const { isLoading, results } = useGetWeatherForCities(cities) if (isLoading) { - return
Loading...
+ return
Laster...
} const cityWeather = cities.map((city, index) => ({ diff --git a/src/components/starButton.tsx b/src/components/starButton.tsx index 7831590..8d35f7b 100644 --- a/src/components/starButton.tsx +++ b/src/components/starButton.tsx @@ -10,9 +10,9 @@ const StarButton = ({ cityName }: { cityName: string }) => { return ( ) 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; From fc0aa6d0ec47bd8fb26a5c1f4486a3a71b761c8e Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 18 Sep 2026 01:27:11 +0200 Subject: [PATCH 2/6] search favorite works --- src/App.tsx | 2 +- src/components/ViewCity.tsx | 8 ++++---- src/components/listItem.tsx | 4 +++- src/components/location-list.tsx | 13 +++++++------ src/components/starButton.tsx | 9 ++++++--- src/hooks/useStorage.ts | 9 +++++---- src/services/storage.ts | 4 +++- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 27b10c2..78d622d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,7 @@ function App() {
- +
diff --git a/src/components/ViewCity.tsx b/src/components/ViewCity.tsx index c9d59b3..1bba61e 100644 --- a/src/components/ViewCity.tsx +++ b/src/components/ViewCity.tsx @@ -3,11 +3,11 @@ import useGetCityCoordinates from "../hooks/fetchCities/useGetCityCoordinates" import StarButton from "./starButton" interface ViewCityProp { - city: string + cityName: string } 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 +101,7 @@ export default function ViewCity(arg: ViewCityProp) { fontWeight: "500", }} > - {arg.city} + {arg.cityName}

- +

{ alignItems: "center", }} > - + {item.name}: {typeof item.precipitation === "number" && item.precipitation > 0.6 diff --git a/src/components/location-list.tsx b/src/components/location-list.tsx index 7112473..9407808 100644 --- a/src/components/location-list.tsx +++ b/src/components/location-list.tsx @@ -48,15 +48,14 @@ interface LocationListProps { const LocationList = ({ onCitySelect }: LocationListProps) => { const { storage } = useAppStorage() - if (storage.starredLocations.length > 0) { for (const starredLocation of storage.starredLocations) { - if (!cities.some((city) => city.name === starredLocation)) { - cities.unshift({ name: starredLocation, lat: 0, lon: 0}) + if (!cities.some((city) => city.name === starredLocation.name)) { + cities.unshift(starredLocation) } - }} - - const { isLoading, results } = useGetWeatherForCities(cities) + }} + + const { isLoading, results } = useGetWeatherForCities(cities) if (isLoading) { return
Laster...
@@ -64,6 +63,8 @@ const LocationList = ({ onCitySelect }: LocationListProps) => { 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]), diff --git a/src/components/starButton.tsx b/src/components/starButton.tsx index 8d35f7b..38e721c 100644 --- a/src/components/starButton.tsx +++ b/src/components/starButton.tsx @@ -1,15 +1,18 @@ +import type { City } from "../hooks/fetchWeather/types" import { useAppStorage } from "../hooks/useStorage" -const StarButton = ({ cityName }: { cityName: string }) => { +const StarButton = ({ city }: { city: City }) => { const { storage, toggleStarredLocation } = useAppStorage() + const isStarred = storage.starredLocations.some((starredCity) => starredCity.name === city.name) + const handleClick = () => { - toggleStarredLocation(cityName) + toggleStarredLocation(city) } return (
- +
{ +const listItem = ({ city, storage, toggleStarredLocation }: { city: cityWeather; 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()}°` : "—"}

    @@ -43,7 +45,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 9407808..66a5321 100644 --- a/src/components/location-list.tsx +++ b/src/components/location-list.tsx @@ -1,7 +1,7 @@ import type { WeatherApiResponse, City } from "../hooks/fetchWeather/types" import { useGetWeatherForCities } from "../hooks/fetchWeather/useGetWeather" import ListItem from "./listItem" -import { useAppStorage} from "../hooks/useStorage" +import type { AppStorage } from "../services/storage" const cities: City[] = [ { name: "Sørumsand", lat: 59.98621, lon: 11.24154 }, @@ -44,11 +44,15 @@ const getNextDays = (data?: WeatherApiResponse) => { interface LocationListProps { onCitySelect: (cityName: string) => void + storage: AppStorage + toggleStarredLocation: (city: City) => void } -const LocationList = ({ onCitySelect }: LocationListProps) => { - const { storage } = useAppStorage() +const LocationList = ({ onCitySelect, storage, toggleStarredLocation }: LocationListProps) => { if (storage.starredLocations.length > 0) { + if (storage.starredLocations.length > 4) { + cities.splice(0, 4) + } for (const starredLocation of storage.starredLocations) { if (!cities.some((city) => city.name === starredLocation.name)) { cities.unshift(starredLocation) @@ -109,7 +113,7 @@ const LocationList = ({ onCitySelect }: LocationListProps) => { key={item.name} style={{ width: "80%" }} > - +
  • ))} diff --git a/src/components/starButton.tsx b/src/components/starButton.tsx index 38e721c..530e9c8 100644 --- a/src/components/starButton.tsx +++ b/src/components/starButton.tsx @@ -1,8 +1,7 @@ import type { City } from "../hooks/fetchWeather/types" -import { useAppStorage } from "../hooks/useStorage" +import type { AppStorage } from "../services/storage"; -const StarButton = ({ city }: { city: City }) => { - const { storage, toggleStarredLocation } = useAppStorage() +const StarButton = ({ city, storage, toggleStarredLocation }: { city: City; storage: AppStorage; toggleStarredLocation: (city: City) => void }) => { const isStarred = storage.starredLocations.some((starredCity) => starredCity.name === city.name) diff --git a/src/hooks/useStorage.ts b/src/hooks/useStorage.ts index fc21ae1..b8074a2 100644 --- a/src/hooks/useStorage.ts +++ b/src/hooks/useStorage.ts @@ -24,9 +24,9 @@ export function useAppStorage() { const toggleStarredLocation = useCallback( (location: City) => { updateStorage((prev) => { - const exists = prev.starredLocations.includes(location) + const exists = prev.starredLocations.some((loc) => loc.name === location.name) const starredLocations = exists - ? prev.starredLocations.filter((id) => id !== location) + ? prev.starredLocations.filter((loc) => loc.name !== location.name) : [...prev.starredLocations, location] return { ...prev, starredLocations } From 6b246e5a43c7276deb224912ef5d1fb39eb6573d Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 18 Sep 2026 17:54:04 +0200 Subject: [PATCH 4/6] state actually works --- src/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 84255ec..3b348ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,14 +10,14 @@ function App() { const handleSearch = (query: string) => { setSelectedCity(query) } - const storageHook = useAppStorage() + const {storage, toggleStarredLocation} = useAppStorage() return ( <>
    - + - +
    From f926fc13c7c240267740b66faaf1d29ca12c62e8 Mon Sep 17 00:00:00 2001 From: Kristian Date: Fri, 18 Sep 2026 18:24:31 +0200 Subject: [PATCH 5/6] tests and everything works --- src/App.tsx | 9 ++++-- src/components/ViewCity.tsx | 15 ++++++++-- src/components/listItem.tsx | 13 +++++++-- src/components/location-list.tsx | 39 +++++++++++++------------ src/components/starButton.tsx | 20 +++++++++---- src/hooks/useStorage.ts | 4 ++- tests/components/ViewCity.test.tsx | 6 ++-- tests/components/listItem.test.tsx | 6 +++- tests/components/location-list.test.tsx | 2 +- 9 files changed, 76 insertions(+), 38 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 3b348ea..fd4b486 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,14 +10,17 @@ function App() { const handleSearch = (query: string) => { setSelectedCity(query) } - const {storage, toggleStarredLocation} = useAppStorage() + const storageHook = useAppStorage() return ( <>
    - + - +
    diff --git a/src/components/ViewCity.tsx b/src/components/ViewCity.tsx index 1381e37..8f4c7aa 100644 --- a/src/components/ViewCity.tsx +++ b/src/components/ViewCity.tsx @@ -6,8 +6,10 @@ import type { City } from "../hooks/fetchWeather/types" interface ViewCityProp { cityName: string - storage: AppStorage - toggleStarredLocation: (city: City) => void + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } } export default function ViewCity(arg: ViewCityProp) { @@ -118,7 +120,14 @@ export default function ViewCity(arg: ViewCityProp) {

    - +
    void }) => { +const listItem = ({ + city, + storageHook, +}: { + city: cityWeather + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } +}) => { return (
  • - + {city.name}: {typeof city.precipitation === "number" && city.precipitation > 0.6 diff --git a/src/components/location-list.tsx b/src/components/location-list.tsx index 66a5321..e6640bd 100644 --- a/src/components/location-list.tsx +++ b/src/components/location-list.tsx @@ -26,11 +26,7 @@ const getNextDays = (data?: WeatherApiResponse) => { return ( date >= now && date.getUTCHours() === 12 && - date < - new Date( - now.getTime() + - 3 * 24 * 60 * 60 * 1000 - ) + date < new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000) ) }) .map((entry) => ({ @@ -44,22 +40,27 @@ const getNextDays = (data?: WeatherApiResponse) => { interface LocationListProps { onCitySelect: (cityName: string) => void - storage: AppStorage - toggleStarredLocation: (city: City) => void + storageHook?: { + storage: AppStorage + toggleStarredLocation: (city: City) => void + } } -const LocationList = ({ onCitySelect, storage, toggleStarredLocation }: LocationListProps) => { - if (storage.starredLocations.length > 0) { - if (storage.starredLocations.length > 4) { - cities.splice(0, 4) - } - for (const starredLocation of storage.starredLocations) { - if (!cities.some((city) => city.name === starredLocation.name)) { - cities.unshift(starredLocation) +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) + } + } + + const { isLoading, results } = useGetWeatherForCities(cities) if (isLoading) { return
    Laster...
    @@ -113,7 +114,7 @@ const LocationList = ({ onCitySelect, storage, toggleStarredLocation }: Location key={item.name} style={{ width: "80%" }} > - +
  • ))} diff --git a/src/components/starButton.tsx b/src/components/starButton.tsx index 530e9c8..83a0408 100644 --- a/src/components/starButton.tsx +++ b/src/components/starButton.tsx @@ -1,12 +1,22 @@ import type { City } from "../hooks/fetchWeather/types" -import type { AppStorage } from "../services/storage"; +import type { AppStorage } from "../services/storage" -const StarButton = ({ city, storage, toggleStarredLocation }: { city: City; storage: AppStorage; toggleStarredLocation: (city: City) => void }) => { - - const isStarred = storage.starredLocations.some((starredCity) => starredCity.name === city.name) +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(city) + storageHook?.toggleStarredLocation(city) } return ( diff --git a/src/hooks/useStorage.ts b/src/hooks/useStorage.ts index b8074a2..4725873 100644 --- a/src/hooks/useStorage.ts +++ b/src/hooks/useStorage.ts @@ -24,7 +24,9 @@ export function useAppStorage() { const toggleStarredLocation = useCallback( (location: City) => { updateStorage((prev) => { - const exists = prev.starredLocations.some((loc) => loc.name === location.name) + const exists = prev.starredLocations.some( + (loc) => loc.name === location.name + ) const starredLocations = exists ? prev.starredLocations.filter((loc) => loc.name !== location.name) : [...prev.starredLocations, location] 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", () => { From f320c869ae75f13034e4ff9ef4b40fb3b649601a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20Gr=C3=A6sberg?= Date: Fri, 18 Sep 2026 18:32:43 +0200 Subject: [PATCH 6/6] Add automatic lint fixing for simple issues --- package.json | 1 + 1 file changed, 1 insertion(+) 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",