Paste Shaver

Text Content

# Cursor Prompt: Contractor Service Map

## Goal
Build a web app that displays contractors on an interactive map. Users enter a location (or use geolocation), filter by service type, and see which contractors operate in that area. Each contractor has a defined **service area** (polygon or radius), and those areas render as overlays on the map. The user should be able to instantly see *"for this address + this service, here are my options."*

---

## Tech Stack

**Framework**
- Next.js 15 (App Router) + TypeScript
- React 19

**Map**
- **MapLibre GL JS** (free, open-source Mapbox fork — no usage caps)
- OpenStreetMap tiles for dev; swap to MapTiler or Protomaps for prod
- `react-map-gl` v8 wrapper for clean React bindings

**Backend / Data**
- Supabase (Postgres + PostGIS + Auth + Storage)
- PostGIS for geospatial queries (`ST_Contains`, `ST_DWithin`, `ST_Intersects`)
- Drizzle ORM for type-safe queries

**UI**
- Tailwind CSS v4
- shadcn/ui for components (Sheet, Command, Combobox, Card, Badge)
- Lucide icons

**State & Data Fetching**
- Zustand for filter state (service type, location, radius)
- TanStack Query for server state and caching

**Geocoding**
- Mapbox Geocoding API or Nominatim (free, OSM-based) for address → lat/lng

**Deployment**
- Vercel (frontend) + Supabase (db)

---

## Data Model

```sql
-- contractors
id uuid pk
name text
slug text unique
phone text
email text
website text
logo_url text
rating numeric(2,1)
verified boolean
created_at timestamptz

-- service_types (lookup table)
id uuid pk
name text  -- "Plumbing", "Roofing", "HVAC", "Electrical"
slug text
icon text

-- contractor_services (join — which services a contractor offers)
contractor_id uuid fk
service_type_id uuid fk

-- service_areas (the polygons/radii each contractor covers)
id uuid pk
contractor_id uuid fk
area_type text  -- 'polygon' | 'radius'
geometry geography(Polygon, 4326)  -- PostGIS
center geography(Point, 4326)       -- for radius type
radius_meters integer                -- for radius type
color text                           -- overlay color for this contractor
```

**Key PostGIS query** — "find contractors whose service area contains this point and who offer this service":
```sql
SELECT DISTINCT c.*
FROM contractors c
JOIN contractor_services cs ON cs.contractor_id = c.id
JOIN service_areas sa ON sa.contractor_id = c.id
WHERE cs.service_type_id = $1
  AND ST_Contains(sa.geometry::geometry, ST_SetSRID(ST_MakePoint($2, $3), 4326));
```

---

## Core Features

### 1. Map View (primary)
- Full-bleed MapLibre map, centered on user's geolocation or a default region
- Contractor pins clustered when zoomed out (using `supercluster` or MapLibre's built-in clustering)
- Click pin → opens contractor detail panel (right-side Sheet on desktop, bottom Sheet on mobile)
- Service area polygons render as **semi-transparent overlays** (0.2 opacity fill, 0.6 opacity stroke), each contractor gets a distinct color
- Toggle to show/hide overlays for all visible contractors (default: show only on hover or when contractor is selected, to prevent visual chaos)

### 2. Filter Panel (left sidebar on desktop, collapsible Sheet on mobile)
- **Location input**: address autocomplete (Nominatim or Mapbox Geocoding) + "Use my location" button
- **Service type**: multi-select chips (Plumbing, Roofing, HVAC, etc.) — single select for MVP
- **Radius**: optional slider (1 mi → 50 mi) if filtering by point + radius instead of "who covers this address"
- **Rating**: min star rating slider
- **Verified only**: toggle

When filters change → refetch contractors → update pins and overlays in place.

### 3. Results List
- Below or alongside the map: card list of matching contractors, sorted by relevance (distance, rating, verified status)
- Each card: logo, name, service types as badges, rating, "Contact" CTA
- Clicking a card highlights that contractor's pin + overlay on the map

### 4. Contractor Detail
- Slide-in panel with full info, all service types offered, full service area visible on map, contact methods, link to external site

---

## UX Details That Matter

- **Overlay strategy**: Don't show every contractor's polygon at once — it's noise. Default behavior:
  - Show pins always
  - Show one contractor's overlay when their pin or list card is hovered/selected
  - "Show all overlays" toggle for power users
- **Empty state**: if filters return zero contractors, show a clear "No contractors cover this area for [Service]. Try expanding your radius or removing filters."
- **URL state**: encode filters in the URL (`?service=plumbing&lat=35.7&lng=-78.6`) so users can share results
- **Mobile**: map takes the full screen; filter and results panels are bottom sheets

---

## Project Structure

```
/app
  /map                    # main map page
    page.tsx
    layout.tsx
  /api
    /contractors
      route.ts            # GET — filtered contractor query
    /geocode
      route.ts            # address lookup proxy
/components
  /map
    ContractorMap.tsx     # MapLibre wrapper
    ContractorPin.tsx
    ServiceAreaOverlay.tsx
    MapClusters.tsx
  /filters
    FilterPanel.tsx
    LocationInput.tsx
    ServiceSelector.tsx
  /contractors
    ContractorCard.tsx
    ContractorDetail.tsx
    ResultsList.tsx
/lib
  /db
    schema.ts             # Drizzle schema
    queries.ts            # geo queries
  /map
    layers.ts             # MapLibre layer definitions
    colors.ts             # color assignment per contractor
  /stores
    filters.ts            # Zustand store
```

---

## MVP Scope (Phase 1)

Cut to these features only — get it working end-to-end first:

1. Map renders with contractor pins from Supabase
2. One filter: service type dropdown
3. One filter: address input that recenters map + filters contractors whose area contains that point
4. Click pin → show contractor name, services, rating in a popup
5. Show service area overlay only for the selected contractor

**Defer to Phase 2**: clustering, multi-select filters, ratings filter, results list, URL state, mobile sheets, contractor detail page, auth, admin panel for contractors to draw their own service areas.

---

## Build Order

1. Scaffold Next.js + Tailwind + shadcn
2. Set up Supabase project, enable PostGIS extension, create tables, seed with 5–10 fake contractors with hand-drawn service areas (use geojson.io to draw test polygons)
3. Build the Drizzle schema and one API route: `GET /api/contractors?service=X&lat=Y&lng=Z`
4. Build the map component with pins
5. Add the filter panel wired to Zustand
6. Add overlay rendering for the selected contractor
7. Test the full flow: enter address → pick service → see matching contractors and one overlay at a time

---

## Notes / Decisions to Make Before Coding

- **Service areas**: who draws them? If contractors self-onboard, you need an admin tool with a polygon-drawing UI (mapbox-gl-draw / maplibre-gl-draw). For MVP, draw them yourself in geojson.io and paste them in.
- **Tile provider cost**: OSM raw tiles are free but rate-limited and not for heavy production. Plan to swap to MapTiler ($) or self-host Protomaps (free, more setup) before launch.
- **Geocoding**: Nominatim is free but rate-limited (1 req/sec). For real traffic, Mapbox Geocoding or Google's API.

---

## What to tell Cursor

> Build the MVP described above. Start with the Next.js scaffold, Supabase schema, and the single contractor API route. Then build the map page with pins, the service-type filter, and address-based filtering. Use MapLibre + react-map-gl, not Mapbox. Use Drizzle for queries. Keep filter state in a Zustand store. Don't add features outside the MVP scope until I confirm the core flow works.