Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 7 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("Sørumsand")
const handleSearch = (query: string) => {
setSelectedCity(query)
}
const storageHook = useAppStorage()

return (
<>
<section id="center">
<LocationList onCitySelect={setSelectedCity} />
<LocationList
onCitySelect={setSelectedCity}
storageHook={storageHook}
/>
<SearchBar onSearch={handleSearch} />
<ViewCity city={selectedCity} />
<ViewCity cityName={selectedCity} storageHook={storageHook} />
</section>
<section id="spacer"></section>
</>
Expand Down
21 changes: 17 additions & 4 deletions src/components/ViewCity.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -101,7 +107,7 @@ export default function ViewCity(arg: ViewCityProp) {
fontWeight: "500",
}}
>
{arg.city}
{arg.cityName}
</h2>
<p
style={{
Expand All @@ -114,7 +120,14 @@ export default function ViewCity(arg: ViewCityProp) {
</p>
</div>
</div>
<StarButton cityName={arg.city} />
<StarButton
city={{
name: arg.cityName,
lat: data?.result[0]?.latitude,
lon: data?.result[0]?.longitude,
}}
storageHook={arg.storageHook}
/>
</div>

<div
Expand Down
29 changes: 21 additions & 8 deletions src/components/listItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import "../App.css"
import type { City } from "../hooks/fetchWeather/types"
import type { AppStorage } from "../services/storage"
import StarButton from "./starButton"

type cityWeather = {
name: string
lat: number
lon: number
temperature?: number
precipitation?: number
nextDays?: { time: string; temperature?: number; precipitation?: number }[]
}

const listItem = ({ city: item }: { city: cityWeather }) => {
const listItem = ({
city,
storageHook,
}: {
city: cityWeather
storageHook?: {
storage: AppStorage
toggleStarredLocation: (city: City) => void
}
}) => {
return (
<li key={item.name} className="list-item">
<li key={city.name} className="list-item">
<span
style={{
display: "flex",
Expand All @@ -19,16 +32,16 @@ const listItem = ({ city: item }: { city: cityWeather }) => {
alignItems: "center",
}}
>
<StarButton cityName={item.name} />
{item.name}:
<StarButton city={city} storageHook={storageHook} />
{city.name}:
<span>
{typeof item.precipitation === "number" && item.precipitation > 0.6
{typeof city.precipitation === "number" && city.precipitation > 0.6
? "🌧️"
: "⛅"}
</span>
<p style={{ color: "#ff3232" }}>
{typeof item.temperature === "number"
? `${item.temperature.toFixed()}°`
{typeof city.temperature === "number"
? `${city.temperature.toFixed()}°`
: "—"}
</p>
</span>
Expand All @@ -41,7 +54,7 @@ const listItem = ({ city: item }: { city: cityWeather }) => {
alignSelf: "end",
}}
>
{item.nextDays?.map((day) => (
{city.nextDays?.map((day) => (
<span key={day.time} style={{ color: "#ff3232", marginRight: "8px" }}>
{typeof day.precipitation === "number" && day.precipitation > 0.6
? "🌧️"
Expand Down
35 changes: 25 additions & 10 deletions src/components/location-list.tsx
Original file line number Diff line number Diff line change
@@ -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 },
]
Expand All @@ -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) => ({
Expand All @@ -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 <div>Loading...</div>
return <div>Laster...</div>
}

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]),
Expand Down Expand Up @@ -99,7 +114,7 @@ const LocationList = ({ onCitySelect }: LocationListProps) => {
key={item.name}
style={{ width: "80%" }}
>
<ListItem city={item} />
<ListItem city={item} storageHook={storageHook} />
</div>
))}
</ul>
Expand Down
26 changes: 19 additions & 7 deletions src/components/starButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<button onClick={handleClick}>
{storage.starredLocations.includes(cityName) ? (
<span style={{ color: "#ffee00" }}></span>
{isStarred ? (
<span></span>
) : (
<span style={{ color: "#000000" }}></span>
<span style={{ color: "#000000", fontSize: "20px" }}></span>
)}
</button>
)
Expand Down
11 changes: 7 additions & 4 deletions src/hooks/useStorage.ts
Original file line number Diff line number Diff line change
@@ -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<AppStorage>(getData)
Expand All @@ -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 }
})
Expand Down
1 change: 1 addition & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ ul {
}

button {
color: #000;
border: none;
background: transparent;
cursor: pointer;
Expand Down
4 changes: 3 additions & 1 deletion src/services/storage.ts
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions tests/components/ViewCity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("ViewCity", () => {
result: [{ latitude: 59.9, longitude: 10.7, country: "Norway" }],
})

render(<ViewCity city="Oslo" />)
render(<ViewCity cityName="Oslo" />)

expect(screen.getByText("Oslo")).toBeTruthy()
expect(screen.getByText("Norway")).toBeTruthy()
Expand All @@ -68,7 +68,7 @@ describe("ViewCity", () => {
it("should render a loading message while weather data is loading", () => {
mockUseGetWeather.mockReturnValue({ isLoading: true, error: null })

render(<ViewCity city="Oslo" />)
render(<ViewCity cityName="Oslo" />)

expect(screen.getByText("Laster...")).toBeTruthy()
})
Expand All @@ -79,7 +79,7 @@ describe("ViewCity", () => {
error: new Error("Request failed"),
})

render(<ViewCity city="Oslo" />)
render(<ViewCity cityName="Oslo" />)

expect(screen.getByText("Det skjedde en feil")).toBeTruthy()
})
Expand Down
6 changes: 5 additions & 1 deletion tests/components/listItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ describe("ListItem", () => {
<ListItem
city={{
name: "Oslo",
lat: 59.9139,
lon: 10.7522,
temperature: 10.6,
precipitation: 0.7,
nextDays: [
Expand All @@ -28,7 +30,9 @@ describe("ListItem", () => {
})

it("should show fallback values when weather data is unavailable", () => {
render(<ListItem city={{ name: "Trondheim" }} />)
render(
<ListItem city={{ name: "Trondheim", lat: 63.4106, lon: 10.4133 }} />
)

expect(screen.getByText("⛅")).toBeTruthy()
expect(screen.getByText("—")).toBeTruthy()
Expand Down
2 changes: 1 addition & 1 deletion tests/components/location-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("LocationList", () => {

render(<LocationList onCitySelect={() => undefined} />)

expect(screen.getByText("Loading...")).toBeTruthy()
expect(screen.getByText("Laster...")).toBeTruthy()
})

it("should render a weather row for every configured city", () => {
Expand Down