# 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` |