Skip to content

feat(compare): side-by-side compare two listings at /compare?a=&b= - #12

Merged
timothygithinji merged 1 commit into
mainfrom
feat/side-by-side-compare
May 28, 2026
Merged

feat(compare): side-by-side compare two listings at /compare?a=&b=#12
timothygithinji merged 1 commit into
mainfrom
feat/side-by-side-compare

Conversation

@timothygithinji

@timothygithinji timothygithinji commented May 28, 2026

Copy link
Copy Markdown
Owner

First of the "other improvements" — direct fit for your 2-person blind-veto workflow.

Problem

A 2-person household running blind-veto shortlists ends up with 4–8 mutually-shortlisted properties. Picking between them means tabbing through full listing detail pages, holding photos / costs / commute / EPC / crime in your head. The decision keeps slipping.

Fix

/compare?a=<clusterId>&b=<clusterId> — one page, two listings, stripped to the decision-changing fields. Two useSuspenseQuery calls run against the existing getListingDetail server function in parallel; no new endpoint.

┌─────────────────────────────────────────────────────────────┐
│  Compare  (Side by side)                  ←  Shortlist      │
├──────────────────────────┬──────────────────────────────────┤
│  A                       │  B                               │
│  [hero photo]            │  [hero photo]                    │
│  £1,900/mo               │  £2,100/mo                       │
│  Wakefield Road, N11     │  Highgate Studios, N6            │
│  ┌──────────────────┐    │  ┌──────────────────┐            │
│  │ Costs            │    │  │ Costs            │            │
│  │ Rent       £1900 │    │  │ Rent       £2100 │            │
│  │ Council tax £190 │    │  │ Council tax £232 │            │
│  │ Deposit    £267  │    │  │ Deposit    £262  │            │
│  │ TRUE     £2,357  │    │  │ TRUE     £2,594  │            │
│  └──────────────────┘    │  └──────────────────┘            │
│  Stands out: chips...    │  Stands out: chips...            │
│  Key stats: EPC C, ...   │  Key stats: EPC B, ...           │
│  Stations: Bounds Green  │  Stations: Highgate              │
│                          │                                  │
└──────────────────────────┴──────────────────────────────────┘

What's in each column

CompareColumn (src/components/compare/compare-column.tsx):

  • Hero photo (small)
  • Address + headline price + AI summary
  • Costs — reuses <CostsCard> from item 6 (rent, council tax, service charge, ground rent, amortised deposit, true monthly total)
  • Stands out — up to 3 highlight chips + up to 3 watchout chips (filtered through the relevance pass from item 3)
  • Key stats — EPC, broadband, crime with area-baseline comparison (item 4)
  • Nearest stations — walking + transit minutes (item 5)
  • "Open full listing →" link with from=compare so the back button flows correctly

Layouts

  • Desktop (lg+): AdminSidebar shell + grid grid-cols-2 gap-6, A left / B right, scrolling together
  • Mobile (<lg): single column with an A/B tab switcher at the top — a stacked dump is unusable on a phone; tabs keep the comparison glanceable

Origin tracking

listingFromOriginSchema gains a compare variant so the listing-detail back button + sidebar breadcrumb route through Shortlist when the user came from /compare. routeTree.gen.ts was regenerated to register the new route.

v1 scope intentionally limited

This PR ships the route + columns. The Shortlist "pick two to compare" selector UX is a v1.1 follow-up — for now the page is URL-driven:

  1. Open a listing in your shortlist
  2. Copy the cluster ID from /listings/<clusterId>
  3. Do the same with a second listing
  4. Visit /compare?a=<first>&b=<second>

Landing on /compare without both IDs surfaces a friendly empty state that points back to Shortlist. The selector UX is a ~30-line addition to MatchRow + the Shortlist route I can ship as a tiny follow-up if you want.

Verification

  • bun run typecheck
  • bun run lint:ci
  • bun run test298 / 298 pass

No new tests this PR — CompareColumn is presentational logic over an existing data shape, and every section it composes (CostsCard, crime baseline label, station routes) is already covered by tests in PRs #9 / #10 / #11.

Builds on every PR before it

The compare view is the payoff that makes all six items shine at once: better Costs (PR #11), filtered Stands-out (#8), area-baseline crime (#9), walking/transit to stations (#10), area search supplying the data (#7), and responsive chrome handling both viewports (#5/#6). Worth seeing on prod data.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a side-by-side property comparison view. Compare two properties simultaneously with details including photos, prices, costs, verdicts, key statistics, and nearest stations. The interface adapts responsively: desktop displays two columns, whilst mobile uses tabs to switch between properties.

Review Change Stack

A 2-person household running blind-veto shortlists ends up with 4–8
mutually-shortlisted properties. Picking between them means tabbing
through full listing detail pages, holding photos / costs / commute /
EPC / crime in your head. This adds a single view that shows two
listings side-by-side, stripped to the decision-changing fields.

## Route

/compare?a=<clusterId>&b=<clusterId> — query-params hold the cluster
IDs. Two useSuspenseQuery calls run against the existing
`getListingDetail` server function in parallel; no new endpoint.
Either param missing renders a 'pick a pair from Shortlist' empty
state.

## Layouts

- Desktop (lg+): the standard AdminSidebar shell wraps a
  `grid grid-cols-2 gap-6` row of two `<CompareColumn>`s. A on
  the left, B on the right, both scrolling together.
- Mobile (<lg): a single column with an A/B tab switcher at the top
  — A 600-px-tall stacked dump is unusable on a phone; tabs keep
  the comparison glanceable.

## What's in each column

CompareColumn (`src/components/compare/compare-column.tsx`):

- Hero photo (small).
- Address + headline price + AI summary.
- Costs (reuses `<CostsCard>` — rent, council tax, service
  charge, ground rent, amortised deposit, true monthly total).
- Stands out — up to 3 highlight chips + up to 3 watchout chips.
- Key stats — EPC, broadband, crime (with the area-baseline
  comparison from item 4).
- Nearest stations — walking + transit minutes (from item 5).
- 'Open full listing' link with from=compare so the back button
  flows correctly.

## Origin tracking

`listingFromOriginSchema` gains a `compare` variant so the
listing-detail back button + sidebar breadcrumb route through
Shortlist when the user came from /compare. `routeTree.gen.ts`
re-generated to register the new route.

## v1 scope intentionally limited

This PR ships the route + columns. The Shortlist 'pick two to
compare' selector UX is a v1.1 follow-up — for now the page is
URL-driven: copy two cluster IDs from `/listings/<id>` URLs into
`?a=&b=`. Landing on /compare without both IDs surfaces a
friendly empty state pointing back to Shortlist.

## Verification

- bun run typecheck ✅
- bun run lint:ci ✅
- bun run test — 298 / 298 pass (no new tests; the column reuses
  components already covered by the listing-detail + Costs tests)

The CompareColumn is small (~240 LoC) presentational logic over an
existing data shape — the meaningful behaviour is the route and the
section composition.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 877e378b-6e65-4af4-86fc-821d7782e591

📥 Commits

Reviewing files that changed from the base of the PR and between 9935e4e and 17c807d.

📒 Files selected for processing (4)
  • src/components/compare/compare-column.tsx
  • src/lib/listing-origin.ts
  • src/routeTree.gen.ts
  • src/routes/compare.tsx

Walkthrough

This pull request introduces a new /compare route that enables side-by-side comparison of two property listings. The implementation includes updates to the origin navigation system, router registration, a reusable comparison column component, and responsive page layouts with tabs for mobile.

Changes

Comparison Feature

Layer / File(s) Summary
Comparison origin navigation support
src/lib/listing-origin.ts
The listingFromOriginSchema now accepts compare as a valid origin value. The ORIGIN_TABLE mapping defines back-button navigation to /compare and sidebar highlighting to /shortlist.
Router registration for /compare
src/routeTree.gen.ts
The /compare route is registered in the generated TanStack Router type system through route import, constant definition, and type registry updates including FileRoutesByFullPath, FileRoutesByTo, FileRoutesById, FileRouteTypes, and module augmentation.
Comparison column component
src/components/compare/compare-column.tsx
The CompareColumn component renders listing details in a structured column: hero photo, headline price and address, cost card, highlights/watchouts (up to 3 each), key stats (EPC, broadband, crime), and nearest stations with transit times. Sub-components handle empty states and data formatting.
Comparison page route and layouts
src/routes/compare.tsx
The /compare route enforces authentication, validates a and b query parameters, and preloads both listing details. The page renders two CompareColumn components side-by-side on desktop (in an AdminSidebar layout with a back button) or as a tabbed single-column view on mobile. An empty-state prompt guides users to /shortlist when either parameter is missing.

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@timothygithinji
timothygithinji merged commit 781a3c5 into main May 28, 2026
4 checks passed
@timothygithinji
timothygithinji deleted the feat/side-by-side-compare branch June 21, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant