Skip to content

liya-daisuki/Collinson_Task

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Weather Activity Ranker

Enter a city or town and get a ranking of how desirable it is to visit over the next 7 days for four activities — skiing, surfing, outdoor sightseeing, indoor sightseeing — derived from Open-Meteo weather data.

React (Vite) ──GraphQL──▶ Apollo Server ──▶ Domain (pure scoring) ──▶ Open-Meteo

Quick start

Requires Node 20+.

npm install                 # installs both workspaces

npm run dev:backend         # GraphQL at http://localhost:4000/
npm run dev:frontend        # UI at  http://localhost:5173/

npm run test                # backend unit tests (15)
npm run typecheck           # strict TS, both workspaces
npm run build               # production build, both workspaces

Try Chamonix (skiing/inland), Newquay (real surf/marine data), Tokyo.


Architecture & technical choices

The codebase is a two-workspace npm monorepo (backend/, frontend/). The guiding principle is a strict, one-directional dependency flow with a pure core:

data layer  ─▶  domain layer  ─▶  GraphQL layer  ─▶  index (composition root)
(I/O, fetch)    (pure logic)      (thin mapping)

Backend (backend/src)

Layer Location Responsibility
Data data/openMeteo/ The only code that performs network I/O. Owns the Open-Meteo wire types (types.ts) and maps them into the domain model. Applies timeouts and converts failures into a typed OpenMeteoError.
Domain domain/ Pure, I/O-free business logic. One scorer per activity (domain/activities/*), shared scoring primitives (scoreUtils.ts), and the ranking engine (ranking.ts).
API graphql/ Schema + deliberately thin resolvers: fetch → compute → map. No scoring logic. Dependencies arrive via an injected context.
Root index.ts, config.ts Composition and configuration only.

Key decisions and why:

  • TypeScript everywhere, strict mode (noUncheckedIndexedAccess, exactOptionalPropertyTypes). The brief weights maintainability heavily; strictness catches the array-indexing and optional-field bugs that this kind of "map an external JSON payload" code is prone to.
  • Wire types vs. domain types are separate. Open-Meteo's field names (temperature_2m_max) never leak past data/openMeteo/. The domain speaks tempMaxC with explicit units. A provider change is contained to one mapping function.
  • Scoring is a registry of pure functions. ActivityScorer = (day) => { score, reason }. Adding an activity is one file + one line in activities/index.ts; the ranking engine, GraphQL enum, and UI all derive from that registry. This is the main extensibility seam.
  • Trapezoidal preference curves (band/penalty in scoreUtils.ts) instead of ad-hoc if ladders. Each scorer reads as a declarative statement of "what good weather looks like" and is trivially unit-testable. Every score is 0–100 and carries a human-readable reason for explainability.
  • GraphQL via Apollo Server v5. Chosen over v4 because v4 reached end-of-life on 26 Jan 2026; v5's API is compatible. A typed code (NOT_FOUND / UPSTREAM_ERROR / BAD_USER_INPUT) on every error lets the client branch reliably. Stack traces are suppressed unless NODE_ENV !== "production".
  • Graceful marine degradation. The Marine API only covers coastal cells. Forecast and marine calls run concurrently (Promise.all); a marine failure degrades to waveHeightM: null rather than failing the request, and surfing falls back to a wind-sea proxy. Verified live: Newquay uses real swell, Chamonix uses the wind proxy.

Frontend (frontend/src)

React + Vite + Apollo Client. Intentionally minimal (the brief makes UX secondary): a search box (useLazyQuery), a results list, and small presentational components (RankingCard, ScoreBar). The GraphQL document is typed end-to-end via TypedDocumentNode, so the query and the rendered fields share one source of truth. Loading, empty, error, and result states are all handled.

Scoring model (summary)

Activity Rewards Penalises
Skiing cold (≤ −2 °C ideal), fresh snowfall wind gates the score (multiplier)
Surfing wave height ~1–3 m (real), else wind-sea proxy flat / oversized / cold
Outdoor sightseeing mild temp (16–26 °C), dry, calm rain is the strongest disqualifier
Indoor sightseeing rises as outdoor conditions worsen; never below a floor (always an option)

How AI assisted this project

This project was built with Claude Code as a pair-programming assistant.

  • API contract discovery. Claude fetched and summarised the Open-Meteo geocoding, forecast, and marine docs, which surfaced the modern field names (wind_speed_10m_max, weather_code) and the timezone-required-for-daily constraint — avoiding a round of trial-and-error.
  • Scaffolding & boilerplate. The monorepo, tsconfigs, Apollo/Vite setup, and typed GraphQL document were generated, freeing time for the parts that matter (the scoring model and layering).
  • Scoring design as a conversation. The trapezoidal-curve abstraction and the activity-registry extension point were arrived at by discussing trade-offs, not dictated.
  • Caught a real modelling bug. A unit test written alongside the code failed: a warm, windless day scored 20 for skiing because the calm-wind term contributed on its own. This drove a deliberate redesign — wind now gates (multiplies) the snow/cold score rather than adding to it.
  • Operational catches. Claude flagged that Apollo Server v4 was EOL (upgraded to v5) and that error stack traces were leaking to clients (gated on NODE_ENV).

Every suggestion was reviewed before acceptance; AI accelerated delivery and breadth of checks, it did not replace the design judgement.


Omissions & trade-offs

Deliberately scoped out to keep one well-built vertical slice over four rushed ones (per the brief's "quality > quantity"):

Omitted / shortcut Why How I'd address it
No response caching Out of scope for the core demo. Open-Meteo is hit on every query. Add a short-TTL cache keyed by rounded lat/lon (forecasts change ~hourly); Apollo response cache or a small LRU in the data layer.
Geocoding takes the top match only Keeps the UX a single input box. Ambiguous names (e.g. "Springfield") silently pick one. countryCode is already plumbed through the schema and client; surface a disambiguation dropdown in the UI.
Surfing wind proxy is approximate True surf needs swell period/direction and a coastline model; out of scope. The proxy keeps the activity meaningful inland and is clearly labelled in the reason. Use Marine API swell period/direction where available; treat the proxy as a low-confidence estimate with a confidence flag.
Scoring weights are hand-tuned constants No labelled "good day" dataset to fit against; expert-judgement defaults are defensible and transparent. Externalise weights to config; calibrate against historical data / user feedback.
Frontend tests omitted Test budget spent where risk is highest — the pure scoring/ranking engine (15 unit tests). UI is thin and presentational. Add React Testing Library tests for the search/error/empty states and a mocked Apollo provider.
No rate-limiting / observability / Docker Not needed to demonstrate the architecture. Add request rate-limiting, structured logging/metrics, and a compose file for one-command spin-up.
Single best geocoding result not shown on a map UX is explicitly secondary. Add a small map / coordinates confirmation.

Known shortcuts

  • console.log/console.error in index.ts stand in for a real logger (fine for a single-process demo; would be structured logging in production).
  • No CI pipeline committed; npm run typecheck && npm test && npm run build is the intended gate and all pass locally.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors