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
83 changes: 62 additions & 21 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CityId>(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()
Expand All @@ -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 (
Expand All @@ -63,12 +70,12 @@ function Header({
<select
value={activity}
onChange={(event) =>
onActivityChange(event.target.value as Activity)
onActivityChange(event.target.value as ActivityId)
}
>
{activities.map((value) => (
<option key={value} value={value}>
{value}
{activities.map((activityOption) => (
<option key={activityOption.id} value={activityOption.id}>
{activityOption.name}
</option>
))}
</select>
Expand Down Expand Up @@ -115,36 +122,62 @@ const weatherTypes: Record<number, string> = {
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) *
Expand All @@ -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,
Expand Down Expand Up @@ -241,7 +281,7 @@ function MainWeather({
}: {
city: City;
weather: CityWeather | null;
activity: Activity;
activity: ActivityId;
isLoading: boolean;
error: Error | null;
onRetry: () => void;
Expand All @@ -262,7 +302,8 @@ function MainWeather({
</button>
</div>
<p>
Vurdert for <strong>{activity.toLowerCase()}</strong>.
Vurdert for{' '}
<strong>{getActivityName(activity).toLowerCase()}</strong>.
</p>
{isLoading && <p role="status">Henter værdata…</p>}
{error && (
Expand Down Expand Up @@ -316,11 +357,11 @@ export function App() {
cityIdSchema,
exampleCity.id,
);
const [activity, setActivity] = useStoredState<Activity>(
const [activity, setActivity] = useStoredState<ActivityId>(
'sessionStorage',
't31-activity',
activitySchema,
'Gåtur',
'walking',
);
const [sortMode, setSortMode] = useStoredState<SortMode>(
'sessionStorage',
Expand Down
27 changes: 2 additions & 25 deletions web/src/WeatherExample.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -20,13 +19,6 @@ function renderExample() {
</QueryClientProvider>,
);
}
function renderApp() {
return render(
<QueryClientProvider client={new QueryClient()}>
<App />
</QueryClientProvider>,
);
}
it('renders validated weather and a stable snapshot', async () => {
const { asFragment } = renderExample();
await screen.findByText('14 °C');
Expand Down Expand Up @@ -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 = () => {};
Expand Down
30 changes: 26 additions & 4 deletions web/src/data/cities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { City } from '../types/city';

export const cities: City[] = [
export const cities = [
{
id: 'oslo',
name: 'Oslo',
Expand All @@ -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',
Expand All @@ -31,4 +31,26 @@ export const cities: City[] = [
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 satisfies readonly City[];

export type { CityId } from '../types/city';

export const exampleCity = cities[2];
21 changes: 3 additions & 18 deletions web/src/features/weather/cities.ts
Original file line number Diff line number Diff line change
@@ -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';
6 changes: 0 additions & 6 deletions web/src/type/city.ts

This file was deleted.

File renamed without changes.
16 changes: 16 additions & 0 deletions web/src/types/city.ts
Original file line number Diff line number Diff line change
@@ -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;
}
File renamed without changes.