diff --git a/web/src/App.tsx b/web/src/App.tsx index effcc9f..1cd4222 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,12 +12,19 @@ import { weatherQuery, type CityWeather } from './features/weather/weather'; import { useStoredState } from './shared/storage'; import { WeatherList, type WeatherListItem } from './WeatherList'; -const activities = ['Gåtur', 'Løpetur', 'Skitur'] as const; -type Activity = (typeof activities)[number]; +import { activities } from './data/activities'; +import type { ActivityId } from './types/activity'; + + const sortModes = ['default', 'best', 'temperature', 'rain', 'wind'] as const; type SortMode = (typeof sortModes)[number]; const cityIds = new Set(cities.map(({ id }) => id)); -const activitySchema = z.enum(activities); +const activitySchema = z.enum([ + 'walking', + 'cycling', + 'running', + 'skiing', +]); const sortModeSchema = z.enum(sortModes); const cityIdSchema = z .string() @@ -33,10 +40,10 @@ function Header({ onSortChange, }: { selectedCityId: CityId; - activity: Activity; + activity: ActivityId; sortMode: SortMode; onCityChange: (cityId: CityId) => void; - onActivityChange: (activity: Activity) => void; +onActivityChange: (activity: ActivityId) => void; onSortChange: (sortMode: SortMode) => void; }) { return ( @@ -63,12 +70,12 @@ function Header({ @@ -115,36 +122,62 @@ const weatherTypes: Record = { 99: 'Tordenvær med kraftig hagl', }; -function scoreWeather(weather: CityWeather | null, activity: Activity) { +function scoreWeather( + weather: CityWeather | null, + activity: ActivityId, +) { if (!weather) return Number.NEGATIVE_INFINITY; + const { temperature_2m: temperature, precipitation, wind_speed_10m: wind, } = weather.current; - const profile = { - Gåtur: { + + const profiles: Record< + ActivityId, + { + idealTemperature: number; + temperatureWeight: number; + rainWeight: number; + windWeight: number; + } + > = { + walking: { idealTemperature: 14, temperatureWeight: 2, rainWeight: 16, windWeight: 3, }, - Løpetur: { + cycling: { + idealTemperature: 16, + temperatureWeight: 2, + rainWeight: 20, + windWeight: 5, + }, + running: { idealTemperature: 11, temperatureWeight: 3, rainWeight: 24, windWeight: 4, }, - Skitur: { + skiing: { idealTemperature: -3, temperatureWeight: 4, rainWeight: 10, windWeight: 2, }, - }[activity]; + }; + + const profile = profiles[activity]; + const snowCode = - weather.current.weather_code >= 71 && weather.current.weather_code <= 77; - const snowBonus = activity === 'Skitur' && snowCode ? 35 : 0; + weather.current.weather_code >= 71 && + weather.current.weather_code <= 77; + + const snowBonus = + activity === 'skiing' && snowCode ? 35 : 0; + return ( 100 - Math.abs(temperature - profile.idealTemperature) * @@ -155,6 +188,13 @@ function scoreWeather(weather: CityWeather | null, activity: Activity) { ); } +function getActivityName(activityId: ActivityId) { + return ( + activities.find((activity) => activity.id === activityId)?.name ?? + activityId + ); +} + function DetailsOverlay({ children, onClose, @@ -241,7 +281,7 @@ function MainWeather({ }: { city: City; weather: CityWeather | null; - activity: Activity; + activity: ActivityId; isLoading: boolean; error: Error | null; onRetry: () => void; @@ -262,7 +302,8 @@ function MainWeather({

- Vurdert for {activity.toLowerCase()}. + Vurdert for{' '} + {getActivityName(activity).toLowerCase()}.

{isLoading &&

Henter værdata…

} {error && ( @@ -316,11 +357,11 @@ export function App() { cityIdSchema, exampleCity.id, ); - const [activity, setActivity] = useStoredState( + const [activity, setActivity] = useStoredState( 'sessionStorage', 't31-activity', activitySchema, - 'Gåtur', + 'walking', ); const [sortMode, setSortMode] = useStoredState( 'sessionStorage', diff --git a/web/src/WeatherExample.test.tsx b/web/src/WeatherExample.test.tsx index 4ab9d4c..07480fb 100644 --- a/web/src/WeatherExample.test.tsx +++ b/web/src/WeatherExample.test.tsx @@ -7,10 +7,9 @@ import { QueryClientProvider, } from '@tanstack/react-query'; import { http, HttpResponse } from 'msw'; -import { App } from './App'; import { WeatherExample } from './WeatherExample'; import { WEATHER_URL, fetchWeather } from './features/weather/weather'; -import { cities, exampleCity } from './features/weather/cities'; +import { exampleCity } from './features/weather/cities'; import { server } from './test/server'; import { weatherResponse } from './test/fixtures'; function renderExample() { @@ -20,13 +19,6 @@ function renderExample() { , ); } -function renderApp() { - return render( - - - , - ); -} it('renders validated weather and a stable snapshot', async () => { const { asFragment } = renderExample(); await screen.findByText('14 °C'); @@ -66,22 +58,7 @@ it('shows a loading message until the response arrives', async () => { await screen.findByText('14 °C'); expect(screen.queryByRole('status')).not.toBeInTheDocument(); }); - server.use( - http.get(WEATHER_URL, async () => { - await responseReady; - return HttpResponse.json(weatherResponse); - }), - ); - renderExample(); - try { - expect(screen.getByRole('status')).toHaveTextContent('Henter værdata…'); - expect(screen.queryByRole('alert')).not.toBeInTheDocument(); - } finally { - finishRequest(); - } - await screen.findByText('14 °C'); - expect(screen.queryByRole('status')).not.toBeInTheDocument(); -}); + it('shows an accessible error and loading feedback during a manual retry', async () => { let calls = 0; let finishRetry = () => {}; diff --git a/web/src/data/cities.ts b/web/src/data/cities.ts index df23203..bc92c69 100644 --- a/web/src/data/cities.ts +++ b/web/src/data/cities.ts @@ -1,6 +1,6 @@ import type { City } from '../types/city'; -export const cities: City[] = [ +export const cities = [ { id: 'oslo', name: 'Oslo', @@ -10,8 +10,8 @@ export const cities: City[] = [ { id: 'bergen', name: 'Bergen', - latitude: 60.3913, - longitude: 5.3221, + latitude: 60.393, + longitude: 5.3242, }, { id: 'trondheim', @@ -31,4 +31,26 @@ export const cities: City[] = [ latitude: 69.6492, longitude: 18.9553, }, -]; \ No newline at end of file + { + id: 'bodo', + name: 'Bodø', + latitude: 67.28, + longitude: 14.405, + }, + { + id: 'kristiansand', + name: 'Kristiansand', + latitude: 58.1467, + longitude: 7.9956, + }, + { + id: 'alesund', + name: 'Ålesund', + latitude: 62.4722, + longitude: 6.1495, + }, +] as const satisfies readonly City[]; + +export type { CityId } from '../types/city'; + +export const exampleCity = cities[2]; \ No newline at end of file diff --git a/web/src/features/weather/cities.ts b/web/src/features/weather/cities.ts index 04dc116..a1a3aea 100644 --- a/web/src/features/weather/cities.ts +++ b/web/src/features/weather/cities.ts @@ -1,18 +1,3 @@ -export const cities = [ - { id: 'oslo', name: 'Oslo', latitude: 59.9139, longitude: 10.7522 }, - { id: 'bergen', name: 'Bergen', latitude: 60.393, longitude: 5.3242 }, - { id: 'trondheim', name: 'Trondheim', latitude: 63.4305, longitude: 10.3951 }, - { id: 'stavanger', name: 'Stavanger', latitude: 58.97, longitude: 5.7331 }, - { id: 'tromso', name: 'Tromsø', latitude: 69.6492, longitude: 18.9553 }, - { id: 'bodo', name: 'Bodø', latitude: 67.28, longitude: 14.405 }, - { - id: 'kristiansand', - name: 'Kristiansand', - latitude: 58.1467, - longitude: 7.9956, - }, - { id: 'alesund', name: 'Ålesund', latitude: 62.4722, longitude: 6.1495 }, -] as const; -export type City = (typeof cities)[number]; -export type CityId = City['id']; -export const exampleCity = cities[2]; +export { cities, exampleCity } from '../../data/cities'; +export type { CityId } from '../../data/cities'; +export type { City } from '../../types/city'; \ No newline at end of file diff --git a/web/src/type/city.ts b/web/src/type/city.ts deleted file mode 100644 index fca4ad6..0000000 --- a/web/src/type/city.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface City { - id: string; - name: string; - latitude: number; - longitude: number; -} \ No newline at end of file diff --git a/web/src/type/activity.ts b/web/src/types/activity.ts similarity index 100% rename from web/src/type/activity.ts rename to web/src/types/activity.ts diff --git a/web/src/types/city.ts b/web/src/types/city.ts new file mode 100644 index 0000000..e3751c9 --- /dev/null +++ b/web/src/types/city.ts @@ -0,0 +1,16 @@ +export type CityId = + | 'oslo' + | 'bergen' + | 'trondheim' + | 'stavanger' + | 'tromso' + | 'bodo' + | 'kristiansand' + | 'alesund'; + +export interface City { + id: CityId; + name: string; + latitude: number; + longitude: number; +} \ No newline at end of file diff --git a/web/src/type/weather.ts b/web/src/types/weather.ts similarity index 100% rename from web/src/type/weather.ts rename to web/src/types/weather.ts