feat(compare): side-by-side compare two listings at /compare?a=&b= - #12
Conversation
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.
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (4)
WalkthroughThis pull request introduces a new ChangesComparison Feature
🎯 3 (Moderate) | ⏱️ ~20 minutes Note 🎁 Summarized by CodeRabbit FreeYour 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 |
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. TwouseSuspenseQuerycalls run against the existinggetListingDetailserver function in parallel; no new endpoint.What's in each column
CompareColumn(src/components/compare/compare-column.tsx):<CostsCard>from item 6 (rent, council tax, service charge, ground rent, amortised deposit, true monthly total)from=compareso the back button flows correctlyLayouts
AdminSidebarshell +grid grid-cols-2 gap-6, A left / B right, scrolling togetherOrigin tracking
listingFromOriginSchemagains acomparevariant so the listing-detail back button + sidebar breadcrumb route through Shortlist when the user came from/compare.routeTree.gen.tswas 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:
/listings/<clusterId>/compare?a=<first>&b=<second>Landing on
/comparewithout 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 test— 298 / 298 passNo new tests this PR —
CompareColumnis 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