Skip to content
Merged
72 changes: 72 additions & 0 deletions docs/weather-app-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Weather app architecture

## Overview

The production entry point is `web/src/App.tsx`. `App` owns the user-facing state and composes the reusable city list:

```text
App
├── Header
│ ├── city selector
│ ├── activity selector
│ └── sort selector
└── WeatherList
└── WeatherCard
└── MainWeather (centered overlay while that card is expanded)
```

## State and persistence

- The selected city is stored in `sessionStorage` under `t31-selected-city`.
- The selected activity is stored in `sessionStorage` under `t31-activity`.
- The selected sort mode is stored in `sessionStorage` under `t31-sort-mode`.
- Favorite city IDs are stored in `localStorage` under `t31-favorite-cities`.
- `useStoredState` parses JSON and validates stored values with Zod. Invalid or unavailable values fall back to defaults.
- Weather payloads are not stored in Web Storage. TanStack Query owns the live API responses.

Session storage is used for choices that describe the current visit. Local storage is used for favorites because favorites should survive closing and reopening the browser.

## Main view and navigation

The default view presents the weather cards first. Users can select a city through the city select or a card. A card's details action opens a wider centered overlay without changing the grid row height or aligning the panel to the card column. The panel supplies the main temperature, weather type, precipitation, wind, update time and provider interval, with a close action.

The card list remains visible around the expanded detail. It is sorted from a copied array, so the source city list is never mutated.

## Sorting and ranking

The list supports:

- Original city order.
- Best weather for the selected activity.
- Highest temperature.
- Lowest precipitation.
- Lowest wind speed.

The activity ranking is intentionally small and explainable at this stage. It rewards conditions suitable for the selected activity and subtracts penalties for precipitation and wind. Ranking is presentation logic; the validated API data remains unchanged.

## Accessibility and responsive design

- Native `select` and `button` controls provide keyboard interaction.
- Labels describe all selection controls.
- `main`, `header`, `section`, `article`, `dl`, `dt` and `dd` provide semantic structure.
- Loading and API failures use `role="status"` and `role="alert"`.
- Focus-visible styles are defined in `web/src/styles.css`.
- The layout changes from a horizontal desktop header to stacked mobile controls below 760px.
- Cards use stable grid sizing and remain usable in narrow viewports.

## Verification

Checks run for this implementation:

```bash
npm test
npm run lint
npm run format:check
npm run build --workspace @t31/web
```

The component tests use MSW fixtures, so unit tests do not call Open-Meteo. They cover the existing validated weather example, city/activity state, city navigation, sorting persistence and favorite persistence. Playwright remains the browser-level check for responsive layout and accessibility; install its browsers with `npm run test:e2e:install` before running it locally.

## Scope notes

The ranking formula is a first usable implementation, not a medical or safety forecast. Future issues can refine activity-specific thresholds, add a visible ranking explanation, and add filtering for favorites or weather categories.
66 changes: 66 additions & 0 deletions docs/weather-cards-and-ranking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Weather cards and activity ranking

## New page flow

The weather cards are the default main view. The page opens directly on the city collection so users can compare the available cities at a glance.

Each card provides:

- city name
- weather type
- temperature
- precipitation
- wind
- city selection
- favorite toggle
- a `Se detaljer` action

Selecting a city updates the selected city state. Opening details shows a wider centered overlay. The overlay is independent of the card column, does not change the height of the card grid, and contains the selected city's relevant weather summary and update time. It can be closed with the close button or by clicking outside the panel.

## Activity-specific ranking

The `Best vær for aktivitet` sort option uses a separate weather profile for each activity:

- `Gåtur` prefers mild temperatures, low precipitation and moderate wind.
- `Løpetur` prefers cooler temperatures, no precipitation and low wind.
- `Skitur` prefers temperatures below freezing and gives a bonus to snow weather codes.

The score combines distance from the activity's ideal temperature with precipitation and wind penalties. The resulting ranking is presentation state only; API responses and the city source array are not changed.

When the best-activity sort is active, cards display their current rank. Changing the activity recalculates all scores and can change which city is ranked first.

## State and storage

`App` owns the selected city, activity, sort mode, favorite IDs and currently expanded detail city:

- selected city, activity and sort mode use `sessionStorage`
- favorite city IDs use `localStorage`
- expanded detail state is temporary React state and is not persisted
- weather payloads remain in TanStack Query rather than Web Storage

Stored values are parsed and validated with Zod through `useStoredState`. Invalid storage values fall back to safe defaults.

## Accessibility and responsive behavior

The implementation uses native `select` and `button` controls, accessible labels, semantic headings and `dl` elements for weather values. The expanded main view has a clear heading and close control. Focus-visible styles remain available for keyboard users, and the card grid/control area stacks on narrow screens.

## Verification

The implementation is covered by MSW-backed Vitest tests for:

- the existing validated weather example
- city and activity selection
- card-first detail expansion and navigation
- favorite and sort persistence
- activity-specific ranking changes

The standard checks are:

```bash
npm test
npm run lint
npm run format:check
npm run build --workspace @t31/web
```

Playwright browser checks should be run after installing the configured browsers with `npm run test:e2e:install`.
16 changes: 14 additions & 2 deletions e2e/weather.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { cities } from '../web/src/features/weather/cities';
import { weatherResponse } from '../web/src/test/fixtures';
test('weather example loads accessibly without horizontal overflow', async ({
page,
Expand All @@ -10,11 +11,22 @@ test('weather example loads accessibly without horizontal overflow', async ({
await route.fulfill({ json: weatherResponse });
});
await page.goto('./');
await expect(
page.getByRole('heading', { name: 'Vær i norske byer' }),
).toBeVisible();
await page.getByRole('button', { name: 'Se detaljer for Trondheim' }).click();
await expect(
page.getByRole('heading', { name: 'Været i Trondheim' }),
).toBeVisible();
await expect(page.getByText('14 °C')).toBeVisible();
expect(calls).toBe(1);
await expect(
page
.locator('section[aria-labelledby="selected-weather-title"]')
.getByText('14°'),
).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Vær i norske byer' }),
).toBeVisible();
expect(calls).toBe(cities.length);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
Expand Down
Loading