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