diff --git a/docs/plans/2026-02-28-ui-redesign-implementation.md b/docs/plans/2026-02-28-ui-redesign-implementation.md new file mode 100644 index 0000000..5286cdb --- /dev/null +++ b/docs/plans/2026-02-28-ui-redesign-implementation.md @@ -0,0 +1,482 @@ +# UI/UX Redesign Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Redesign the realestate-crawler frontend from sidebar-filter layout to horizontal-filter-bar layout with React Router, redesigned property cards, refined visual system, and tabbed listing detail. + +**Architecture:** Replace the sidebar FilterPanel with a horizontal FilterBar component. Add react-router-dom for URL-based navigation and deep linking. Extract shared utilities to eliminate duplication. Refine the color palette from plain shadcn neutral to a teal-accented property search theme. All changes are frontend-only — no backend API changes needed. + +**Tech Stack:** React 19, TypeScript, Tailwind CSS 4, shadcn/ui, react-router-dom, Mapbox GL, Vite + +--- + +## Phase 1: Foundation (Router + Shared Utils + Color System) + +### Task 1: Install react-router-dom + +**Files:** +- Modify: `frontend/package.json` + +**Step 1: Install the dependency** + +Run: `cd /Users/viktorbarzin/code/realestate-crawler/frontend && npm install react-router-dom` + +**Step 2: Verify installation** + +Run: `grep react-router-dom package.json` +Expected: `"react-router-dom": "^7..."` + +**Step 3: Commit** + +```bash +cd /Users/viktorbarzin/code/realestate-crawler +git add frontend/package.json frontend/package-lock.json +git commit -m "chore: add react-router-dom dependency" +``` + +--- + +### Task 2: Extract shared utility functions + +Currently `formatDuration`, `formatCurrency`, `formatDate`, `TravelModeIcon`, and `isTerminalStatus` are duplicated across PropertyCard.tsx, ListingDetail.tsx, MobileBottomSheet.tsx, StatsBar.tsx, TaskIndicator.tsx, TaskProgressDrawer.tsx, useTaskProgress.ts, and App.tsx. + +**Files:** +- Create: `frontend/src/utils/format.ts` +- Create: `frontend/src/utils/taskUtils.ts` +- Modify: `frontend/src/components/PropertyCard.tsx` — remove local formatDuration/formatCurrency/formatDate, import from utils +- Modify: `frontend/src/components/ListingDetail.tsx` — same +- Modify: `frontend/src/components/MobileBottomSheet.tsx` — same +- Modify: `frontend/src/components/StatsBar.tsx` — remove local formatCurrency, import from utils +- Modify: `frontend/src/App.tsx` — remove local isTerminalStatus, import from utils +- Modify: `frontend/src/components/TaskIndicator.tsx` — remove local isTerminalStatus/taskStateToResult, import +- Modify: `frontend/src/components/TaskProgressDrawer.tsx` — remove local isTerminalStatus/taskStateToResult, import +- Modify: `frontend/src/hooks/useTaskProgress.ts` — remove local isTerminalStatus, import + +**Step 1: Create `frontend/src/utils/format.ts`** + +Extract these functions by reading them from PropertyCard.tsx (lines ~18-55) and consolidating: +- `formatCurrency(value: number): string` — formats as £X,XXX +- `formatDuration(seconds: number): string` — formats as Xmin / Xhr Xmin +- `formatDate(dateStr: string): string` — formats as "3d ago" / "2w ago" etc. +- `formatPricePerSqm(price: number, sqm: number | null): string | null` + +**Step 2: Create `frontend/src/utils/taskUtils.ts`** + +Extract from App.tsx and TaskIndicator.tsx: +- `isTerminalStatus(status: string): boolean` +- `taskStateToResult(ts: TaskState): TaskResult` + +**Step 3: Update all importing files** to use the shared utils instead of local definitions. Search for each function name and replace local definitions with imports. + +**Step 4: Run tests** + +Run: `cd /Users/viktorbarzin/code/realestate-crawler/frontend && npx vitest run` +Expected: All existing tests pass + +**Step 5: Commit** + +```bash +git add frontend/src/utils/format.ts frontend/src/utils/taskUtils.ts frontend/src/components/ frontend/src/App.tsx frontend/src/hooks/ +git commit -m "refactor: extract shared utility functions to eliminate duplication" +``` + +--- + +### Task 3: Update color palette in index.css + +Replace the achromatic neutral palette with a teal-accented property search palette. + +**Files:** +- Modify: `frontend/src/index.css:44-77` (the `:root` block) + +**Step 1: Update the `:root` CSS variables** + +Change the `:root` block to use teal-accented colors. Key changes: +- `--primary`: change from `oklch(0.205 0 0)` (pure black) to a dark slate `oklch(0.208 0.042 265.755)` (slate 900 with hint of blue) +- `--accent`: change from neutral gray to teal `oklch(0.627 0.14 175.5)` (teal 600) +- `--ring`: update to teal ring color +- Add custom properties for deal indicators: + - `--color-deal-good: oklch(0.696 0.17 162.48)` (emerald 500) + - `--color-deal-above: oklch(0.795 0.184 86.047)` (amber 500) + +Also add these to the `@theme inline` block: +```css +--color-deal-good: var(--deal-good); +--color-deal-above: var(--deal-above); +``` + +**Step 2: Verify Vite HMR picks up the changes** + +The running Vite dev server should hot-reload. Check the browser at localhost:5173. + +**Step 3: Commit** + +```bash +git add frontend/src/index.css +git commit -m "style: update color palette to teal-accented property search theme" +``` + +--- + +### Task 4: Set up React Router with URL-based filter state + +**Files:** +- Modify: `frontend/src/main.tsx` — wrap App in BrowserRouter +- Create: `frontend/src/hooks/useFilterParams.ts` — hook that syncs filter state with URL search params +- Modify: `frontend/src/App.tsx` — use useFilterParams instead of local useState for queryParameters and viewMode + +**Step 1: Update main.tsx** + +```tsx +import { BrowserRouter } from 'react-router-dom'; +// Wrap in +``` + +**Step 2: Create useFilterParams hook** + +This hook: +- Reads filter values from URL search params on mount +- Returns `[filterValues, setFilterValues]` where setFilterValues updates both state and URL +- Reads `viewMode` from the URL pathname (`/map`, `/split`, `/list`, `/saved`) +- Falls back to DEFAULT_FILTER_VALUES for missing params +- Handles the `/callback` route for auth + +Key URL params: `type`, `minPrice`, `maxPrice`, `minBeds`, `maxBeds`, `minSqm`, `maxSqm`, `minPriceSqm`, `maxPriceSqm`, `furnish`, `district`, `lastSeenDays`, `availableFrom`, `sort`, `metric` + +**Step 3: Update App.tsx** + +- Import `useFilterParams` and `useNavigate` from react-router-dom +- Replace `useState(null)` for queryParameters with the hook +- Replace `useState('map')` for viewMode with URL-derived state +- Keep the `/callback` route check but use React Router's Routes/Route + +**Step 4: Test that the app still renders with the router** + +Navigate to `http://localhost:5173/map` in the browser and verify it loads. + +**Step 5: Run tests** + +Run: `cd /Users/viktorbarzin/code/realestate-crawler/frontend && npx vitest run` +Fix any tests that break due to missing router context (wrap test renders in ``). + +**Step 6: Commit** + +```bash +git add frontend/src/main.tsx frontend/src/hooks/useFilterParams.ts frontend/src/App.tsx +git commit -m "feat: add React Router with URL-based filter state and deep linking" +``` + +--- + +## Phase 2: Filter Bar (Replace Sidebar) + +### Task 5: Create the FilterBar component + +This is the core layout change. Create a new horizontal FilterBar that replaces the sidebar FilterPanel. + +**Files:** +- Create: `frontend/src/components/FilterBar.tsx` +- Create: `frontend/src/components/FilterChips.tsx` + +**Step 1: Create FilterBar.tsx** + +A horizontal bar with: +- Compact dropdown selectors for: Price Range (min-max), Bedrooms (min-max), Min Size +- A "More Filters" button that opens a Popover with: Max Size, Price/m² range, Furnishing toggles, District input, Available From date, Last Seen Days, POI travel time filters +- Right-aligned action buttons: "Show Listings" (primary) and "Scrape New" (secondary) +- Uses the same Zod schema and react-hook-form as the current FilterPanel +- Emits the same `onSubmit(action, parameters)` callback + +The bar layout: `flex items-center gap-2 px-4 h-10 bg-muted/50 border-b` + +Use shadcn Popover for dropdown selectors (not full Select components — we want compact inline triggers). + +**Step 2: Create FilterChips.tsx** + +A component that renders active filter values as removable chips: +```tsx +
+ {chips.map(chip => ( + + {chip.label} + + + ))} +
+``` + +Only renders when there are active non-default filters. + +**Step 3: Run tests** + +Run: `npx vitest run` + +**Step 4: Commit** + +```bash +git add frontend/src/components/FilterBar.tsx frontend/src/components/FilterChips.tsx +git commit -m "feat: create horizontal FilterBar and FilterChips components" +``` + +--- + +### Task 6: Integrate FilterBar into App.tsx layout + +**Files:** +- Modify: `frontend/src/App.tsx` — replace FilterPanel sidebar with FilterBar +- Modify: `frontend/src/components/Header.tsx` — add Rent/Buy toggle, remove elements that moved + +**Step 1: Update the desktop layout in App.tsx** + +Replace the current desktop layout structure: +``` +sidebar (FilterPanel w-80) + main content +``` +With: +``` +FilterBar (full-width, below header) +FilterChips (full-width, conditional) +main content (full-width) +StatusBar (full-width) +``` + +Remove the `` render from the desktop layout. Keep it for mobile as a sheet/modal (or replace with FilterBar in a full-screen modal). + +**Step 2: Update Header.tsx** + +- Add Rent/Buy toggle tabs to the header (currently in FilterPanel) +- The toggle should call the filter update function to switch listing_type + +**Step 3: Move VisualizationCard "Color by" control into StatsBar** + +The metric selector (Color by dropdown) currently sits below the filter panel. Move it into the StatsBar component, next to the view mode toggle. + +**Step 4: Update mobile layout** + +Replace the mobile filter Sheet (sidebar slide-in) with a full-screen Dialog/modal containing the FilterBar fields stacked vertically with a sticky "Apply Filters" button at the bottom. + +**Step 5: Verify in browser** + +Navigate to localhost:5173 and verify: +- Filter bar appears below header +- Map takes full viewport width +- Filters work correctly +- Mobile filter opens as modal + +**Step 6: Run tests** + +Run: `npx vitest run` + +**Step 7: Commit** + +```bash +git add frontend/src/App.tsx frontend/src/components/Header.tsx frontend/src/components/StatsBar.tsx +git commit -m "feat: integrate FilterBar into layout, remove sidebar, full-width map" +``` + +--- + +## Phase 3: Property Cards Redesign + +### Task 7: Redesign PropertyCard component + +**Files:** +- Modify: `frontend/src/components/PropertyCard.tsx` + +**Step 1: Redesign the full card variant** + +Update the card layout to match the design: +- Larger image carousel (16:10 aspect ratio via `aspect-[16/10]`) +- Heart and external link buttons overlaid on the image (absolute positioned, top-right) +- Price as the dominant element below image (`text-xl font-bold tracking-tight`) +- Deal indicator as a colored dot + text (using new `--color-deal-good` / `--color-deal-above` CSS vars) +- Key metrics on one line with middle dots: `2 bed · 65 m² · £38/m²` +- Location below metrics +- POI travel times as compact inline badges with mode icons +- Agency + freshness de-emphasized at bottom + +**Step 2: Update compact card variant** + +Ensure PropertyCardCompact also follows the new visual hierarchy (price dominant, location secondary). + +**Step 3: Verify in browser** + +Check both full and compact card rendering. + +**Step 4: Run tests** + +Run: `npx vitest run -- --grep PropertyCard` + +**Step 5: Commit** + +```bash +git add frontend/src/components/PropertyCard.tsx frontend/src/components/PropertyCardCompact.tsx +git commit -m "style: redesign PropertyCard with better visual hierarchy" +``` + +--- + +### Task 8: Redesign ListingDetail with tabbed sections + +**Files:** +- Modify: `frontend/src/components/ListingDetail.tsx` +- Modify: `frontend/src/components/ListingDetailSheet.tsx` + +**Step 1: Update ListingDetailSheet to be larger** + +Change the drawer to take 60% viewport width on desktop (currently `sm:!max-w-lg` → `sm:!max-w-2xl`) and 90vh on mobile. + +**Step 2: Add tabbed sections to ListingDetail** + +Use shadcn Tabs component to organize content into: +- **Overview** tab: Key features list + full description (default active) +- **Travel** tab: POI distances table with all travel modes +- **Price History** tab: Price history timeline (currently inline) +- **Details** tab: Property attributes grid (type, furnishing, council tax, lease, service charge, available from) + +Keep the photo gallery and header (price, metrics, actions) above the tabs. + +**Step 3: Verify in browser** + +Open a listing detail sheet and verify tabs work. + +**Step 4: Commit** + +```bash +git add frontend/src/components/ListingDetail.tsx frontend/src/components/ListingDetailSheet.tsx +git commit -m "feat: redesign listing detail with tabbed sections" +``` + +--- + +## Phase 4: Polish & Cleanup + +### Task 9: Add Error Boundary + +**Files:** +- Create: `frontend/src/components/ErrorBoundary.tsx` +- Modify: `frontend/src/main.tsx` — wrap App in ErrorBoundary + +**Step 1: Create a simple error boundary** + +```tsx +import { Component, type ReactNode } from 'react'; + +interface Props { children: ReactNode; } +interface State { hasError: boolean; error: Error | null; } + +export class ErrorBoundary extends Component { + state: State = { hasError: false, error: null }; + static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } + render() { + if (this.state.hasError) { + return ( +
+
+

Something went wrong

+

{this.state.error?.message}

+ +
+
+ ); + } + return this.props.children; + } +} +``` + +**Step 2: Wrap App in main.tsx** + +**Step 3: Commit** + +```bash +git add frontend/src/components/ErrorBoundary.tsx frontend/src/main.tsx +git commit -m "feat: add error boundary to prevent white-screen crashes" +``` + +--- + +### Task 10: Clean up deprecated FilterPanel references + +**Files:** +- Optionally delete or mark deprecated: `frontend/src/components/FilterPanel.tsx` +- Clean up any remaining references to FilterPanel in App.tsx +- Remove unused sidebar CSS variables from index.css if no longer needed + +**Step 1: Remove FilterPanel from imports and usage in App.tsx** + +If FilterPanel is no longer used anywhere (replaced by FilterBar), remove the import and any dead code. + +**Step 2: Run final test suite** + +Run: `npx vitest run` +Expected: All tests pass + +**Step 3: Build check** + +Run: `npx tsc -b && npx vite build` +Expected: No type errors, successful build + +**Step 4: Commit** + +```bash +git add -A +git commit -m "chore: clean up deprecated FilterPanel references" +``` + +--- + +## Phase 5: Verification + +### Task 11: End-to-end visual verification + +**Step 1: Start the full dev environment** + +Start backend services (Docker compose or local) and the frontend. + +**Step 2: Log in and verify all views** + +- Map view: hexgrid renders, clicking cells shows popups +- Filter bar: all dropdowns work, chips appear/remove correctly +- URL state: changing filters updates URL, refreshing preserves filters +- Split view: map + list side by side +- List view: redesigned cards with correct hierarchy +- Listing detail: tabbed sections work +- Mobile: bottom sheet, filter modal, swipe review +- Saved view: liked listings display correctly + +**Step 3: Take screenshots of before/after** + +Use Chrome CDP at localhost:9222 to capture final state. + +**Step 4: Final commit with any fixes** + +--- + +## Summary of Files Changed + +| Action | File | +|--------|------| +| Create | `frontend/src/utils/format.ts` | +| Create | `frontend/src/utils/taskUtils.ts` | +| Create | `frontend/src/hooks/useFilterParams.ts` | +| Create | `frontend/src/components/FilterBar.tsx` | +| Create | `frontend/src/components/FilterChips.tsx` | +| Create | `frontend/src/components/ErrorBoundary.tsx` | +| Modify | `frontend/package.json` (add react-router-dom) | +| Modify | `frontend/src/main.tsx` (router + error boundary) | +| Modify | `frontend/src/index.css` (color palette) | +| Modify | `frontend/src/App.tsx` (layout restructure, router integration) | +| Modify | `frontend/src/components/Header.tsx` (Rent/Buy toggle) | +| Modify | `frontend/src/components/StatsBar.tsx` (add metric selector) | +| Modify | `frontend/src/components/PropertyCard.tsx` (visual redesign) | +| Modify | `frontend/src/components/PropertyCardCompact.tsx` (visual updates) | +| Modify | `frontend/src/components/ListingDetail.tsx` (tabbed layout) | +| Modify | `frontend/src/components/ListingDetailSheet.tsx` (larger drawer) | +| Modify | `frontend/src/components/TaskIndicator.tsx` (use shared utils) | +| Modify | `frontend/src/components/TaskProgressDrawer.tsx` (use shared utils) | +| Modify | `frontend/src/components/MobileBottomSheet.tsx` (use shared utils) | +| Modify | `frontend/src/hooks/useTaskProgress.ts` (use shared utils) | +| Deprecate | `frontend/src/components/FilterPanel.tsx` |