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
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,XXXformatDuration(seconds: number): string— formats as Xmin / Xhr XminformatDate(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): booleantaskStateToResult(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:rootblock)
Step 1: Update the :root CSS variables
Change the :root block to use teal-accented colors. Key changes:
--primary: change fromoklch(0.205 0 0)(pure black) to a dark slateoklch(0.208 0.042 265.755)(slate 900 with hint of blue)--accent: change from neutral gray to tealoklch(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
viewModefrom the URL pathname (/map,/split,/list,/saved) - Falls back to DEFAULT_FILTER_VALUES for missing params
- Handles the
/callbackroute 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
useFilterParamsanduseNavigatefrom 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
/callbackroute 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-aboveCSS 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-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
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 |