wrongmove/docs/plans/2026-02-28-ui-redesign-implementation.md
Viktor Barzin 706a60c741
docs: add UI/UX redesign implementation plan
11 tasks across 5 phases:
1. Foundation: Router, shared utils, color palette
2. Filter Bar: Replace sidebar with horizontal filter bar
3. Property Cards: Visual redesign with better hierarchy
4. Polish: Error boundary, cleanup
5. Verification: End-to-end visual check
2026-02-28 15:46:30 +00:00

17 KiB

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

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

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:

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

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

import { BrowserRouter } from 'react-router-dom';
// Wrap <App /> in <BrowserRouter>

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<ParameterValues | null>(null) for queryParameters with the hook
  • Replace useState<ViewMode>('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 <MemoryRouter>).

Step 6: Commit

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:

<div className="flex flex-wrap gap-1 px-4 py-1 border-b bg-muted/30">
  {chips.map(chip => (
    <span key={chip.key} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent/10 text-accent text-xs font-medium">
      {chip.label}
      <button onClick={() => onRemove(chip.key)}><X className="h-3 w-3" /></button>
    </span>
  ))}
</div>

Only renders when there are active non-default filters.

Step 3: Run tests

Run: npx vitest run

Step 4: Commit

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 <FilterPanel> 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

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

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-lgsm:!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

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

import { Component, type ReactNode } from 'react';

interface Props { children: ReactNode; }
interface State { hasError: boolean; error: Error | null; }

export class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };
  static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
  render() {
    if (this.state.hasError) {
      return (
        <div className="flex items-center justify-center min-h-screen">
          <div className="text-center p-8 max-w-md">
            <h1 className="text-xl font-bold mb-2">Something went wrong</h1>
            <p className="text-muted-foreground mb-4">{this.state.error?.message}</p>
            <button onClick={() => window.location.reload()} className="px-4 py-2 bg-primary text-primary-foreground rounded-md">
              Reload
            </button>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}

Step 2: Wrap App in main.tsx

Step 3: Commit

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

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