Tinder-style swipe card UI for liking/disliking properties with shared shortlists. Two-phase rollout: core decisions first, sharing second.
6 KiB
Property Decisions Feature — Design Document
Date: 2026-02-21
Problem
Users browse hundreds of listings but have no way to track which properties they're interested in, dismiss ones they've already rejected, or share a shortlist with a partner or flatmate.
Solution
A Tinder-style swipe card review mode where users swipe right (like), left (dislike), or up (skip) on properties. Liked properties are saved; disliked ones are hidden from all views. Shortlists can be shared via link.
Requirements
- Three decision states: Like, Dislike, Neutral (default)
- Disliked properties are fully hidden from map + list views
- Liked properties viewable via filter in existing views and a dedicated "Saved" tab
- Shared shortlists via link for collaboration with partner/flatmate
- Works on mobile (primary: swipe gestures) and desktop (keyboard shortcuts + buttons)
Data Model
listing_decision table
| Column | Type | Description |
|---|---|---|
id |
int, PK | Auto-increment |
user_id |
int, FK -> user | Who made the decision |
listing_id |
int | Rightmove property ID |
listing_type |
str | RENT or BUY |
decision |
str | liked or disliked |
created_at |
datetime | When the decision was made |
updated_at |
datetime | When last changed |
Unique constraint on (user_id, listing_id, listing_type).
shortlist table (Phase 2)
| Column | Type | Description |
|---|---|---|
id |
int, PK | Auto-increment |
owner_id |
int, FK -> user | Creator |
name |
str | e.g. "Flat hunt with Sarah" |
share_token |
str, unique | UUID for share link |
created_at |
datetime |
shortlist_member table (Phase 2)
| Column | Type | Description |
|---|---|---|
shortlist_id |
int, FK | |
user_id |
int, FK |
API Endpoints
Phase 1 — Decisions
| Method | Endpoint | Purpose |
|---|---|---|
PUT |
/api/decisions/{listing_id} |
Set decision (body: {decision, listing_type}) |
GET |
/api/decisions |
Get all user's decisions |
DELETE |
/api/decisions/{listing_id} |
Remove decision (back to neutral) |
Existing /api/listing_geojson and /api/listing_geojson/stream get a decision_filter query parameter: all (default, hides disliked), liked, disliked, undecided, everything.
Phase 2 — Shortlists
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/shortlists |
Create a shortlist |
GET |
/api/shortlists |
List user's shortlists |
GET |
/api/shortlists/{token} |
View shared shortlist (public, no auth) |
POST |
/api/shortlists/{id}/members |
Add member by email |
Swipe Card UI
Entry Point
A "Review" button in the toolbar, next to the existing map/list toggle. Enters swipe review mode.
Card Layout (full-screen mobile)
+-----------------------------+
| <- Back 3 / 47 | header: back + progress
| |
| +-----------------------+ |
| | | |
| | Property Photo | | main image
| | | |
| +-----------------------+ |
| |
| GBP2,150/mo . 2 bed . 65m2| key stats
| GBP33/m2 |
| Clapham, SW4 | location
| |
| Northern Line 22 min | travel times (from POIs)
| Victoria 35 min |
| |
| +-----------------------+ |
| | Mini map snippet | | small static map
| +-----------------------+ |
| |
| [X] [Undo] [Heart] | action buttons
+-----------------------------+
Interactions
- Swipe right -> Like (green overlay, slides off right)
- Swipe left -> Dislike (red overlay, slides off left)
- Swipe up -> Skip / Neutral (slides up, next card)
- Tap buttons -> Same as swipe
- Undo -> Reverses last decision, brings card back
- Tap photo -> Full-screen image gallery
- Tap card body -> Opens Rightmove listing in new tab
- Back -> Exit review mode
Desktop Adaptation
Same card UI centered in modal overlay (max-width ~450px). Keyboard shortcuts: right arrow (like), left arrow (dislike), up arrow (skip), Ctrl+Z (undo).
Card Stack
Show edges of next 2 cards behind current card for depth illusion.
Progress
Counter "3 / 47" — only undecided properties in the review queue.
Filtering
Default Behavior
When logged in, disliked properties are automatically excluded from map and list views. Server-side filtering via join against listing_decision.
Filter Panel
New "Decision" dropdown:
- All (hide disliked) — default
- Liked only
- Undecided only
- Disliked only
- Everything
Saved Tab
New navigation option alongside Map / List. Shows:
- Liked properties in a list, sorted by date saved (most recent first)
- Same card info as regular list view
- Tap to open on Rightmove
- Swipe to remove from saved
Sharing (Phase 2)
- "Share" button in Saved tab opens dialog
- Create named shortlist
- Get share link (
/shared/{token}) - Public read-only view, no auth required
- Authenticated users can join and their likes merge in
- Shows which member liked each property
Technical Decisions
- Animation: react-spring + @use-gesture/react for swipe physics
- State: Decisions stored server-side. Frontend keeps
Map<string, decision>for fast lookup. Optimistic updates with revert on failure. - Backend: Follows existing patterns — new model, repository, service, and route modules.
- Server-side filtering: GeoJSON endpoints join against
listing_decisionto exclude disliked.
Phasing
Phase 1 — Core Decisions + Swipe UI
listing_decisiontable + migration- Decision API endpoints
- Swipe card review mode component
- Filter panel integration
- Saved tab
- Server-side disliked filtering
Phase 2 — Sharing
shortlist+shortlist_membertables + migration- Shortlist API endpoints
- Share link generation + public view
- Member management