diff --git a/docs/plans/2026-02-21-property-decisions-design.md b/docs/plans/2026-02-21-property-decisions-design.md new file mode 100644 index 0000000..6faf9ff --- /dev/null +++ b/docs/plans/2026-02-21-property-decisions-design.md @@ -0,0 +1,184 @@ +# 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) + +1. "Share" button in Saved tab opens dialog +2. Create named shortlist +3. Get share link (`/shared/{token}`) +4. Public read-only view, no auth required +5. Authenticated users can join and their likes merge in +6. 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` 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_decision` to exclude disliked. + +## Phasing + +### Phase 1 — Core Decisions + Swipe UI +- `listing_decision` table + migration +- Decision API endpoints +- Swipe card review mode component +- Filter panel integration +- Saved tab +- Server-side disliked filtering + +### Phase 2 — Sharing +- `shortlist` + `shortlist_member` tables + migration +- Shortlist API endpoints +- Share link generation + public view +- Member management