Contractor marketplace discovery, bid creation, and bid lifecycle management. Terracotta palette — contractor experience.
ListingFilterBar, ListingCard
useQuery(api.marketplace.queries.list) • FlatList
EmptyState with IconCircle hero
Shown when listingsData.items.length === 0
ListingDetailScreen, ImageGallery
useQuery(api.marketplace.queries.get) • useQuery(api.jobs.jobs.get)
CreateBid — pricing section
bidType toggle (precise|range) • PLATFORM_FEE_RATE = 0.085
CreateBid — proposal section
bidExplanation, materialsBreakdown, portfolio attachments
BidPreviewCard — poster POV
Final review before submitBid mutation fires
MyBidsScreen with tab navigation
useQuery(api.jobs.bids.listMyBids) • status: pending|accepted|rejected
BidDetailScreen — shortlisted state
useQuery(api.jobs.bids.get) • posterViewedAt set
BidWonScreen with confetti animation
useQuery(api.jobs.jobs.get) • useQuery(api.jobs.bids.get) • PLATFORM_FEE_RATE
BidDetailScreen — rejected state with AI-generated improvement tips
bid.status === "rejected" • rejectionReason from poster • useQuery(api.jobs.bids.getComparisonInsights)
posterId: Id<posterUsers>
contractorId: Id<contractorUsers> // optional
title: string
status: draft | open | pending_contract | ...
bidCount: number
maxBids: number
bidMode: "precise" | "range"
jobDetails: { budget, photos, timeline, ... }
city / state / zipCode: string
jobId: Id<jobs>
contractorId: Id<contractorUsers>
bidType: "precise" | "range"
bidAmount: number
bidMinAmount / bidMaxAmount: number?
bidExplanation: string
estimatedDays: number?
includesPermits / includesMaterials: boolean
status: pending | accepted | rejected | withdrawn
posterViewedAt: number?
// Discovery
useQuery(api.marketplace.queries.list)
useQuery(api.marketplace.queries.get)
useQuery(api.jobs.jobs.get)
// Bidding
useQuery(api.jobs.bids.listMyBids)
useQuery(api.jobs.bids.get)
useMutation(api.jobs.bids.submit)
useMutation(api.jobs.bids.withdraw)
// bids.submit args:
jobId: Id<jobs>
bidType: "precise" | "range"
bidAmount: number? // precise
bidMinAmount / bidMaxAmount: number? // range
bidExplanation: string
estimatedDays: number?
includesPermits: boolean?
includesMaterials: boolean?
revisedFromBidId: Id<jobBids>?
idempotencyKey: string?