The contractor's primary job discovery experience — marketplace browse, search, filter, bid, and track. Terracotta palette, optimized for speed-to-bid conversion.
FindJobsScreen + EmptyState
useQuery(api.jobs.jobs.listAvailable) returns empty array
FindJobsScreen → JobCard (variant="default")
useQuery(api.jobs.jobs.listAvailable, { limit: 50 })
FiltersPanel (bottom sheet / modal)
Local state for filter values, applied on submit
JobDetailScreen → PhotoCarousel, ScopeList
router.push("/(contractor)/jobs/${jobId}")
CreateBidScreen → BidBreakdown, TimelineSelector
useMutation(api.jobs.bids.create) → router.push("/(contractor)/bids/create")
Your bid of $950.00 has been sent to the homeowner. You’ll be notified when they respond.
BidConfirmationScreen (success state)
Navigated after successful useMutation(api.jobs.bids.create)
MyBidsScreen → BidListItem
useQuery(api.contractor.jobs.getMyBids) with status filter
SavedJobsScreen → SavedJobCard
useQuery(api.contractor.jobs.getSavedJobs), useMutation(api.contractor.jobs.toggleSave)
jobs
_id: Id<"jobs">
posterId: Id<"posterUsers">
title: string
description: string
status: JobStatus
city, state, zipCode: string
jobDetails: { budget, timeline, scope[] }
bidCount: number
maxBids: number
bidMode: "precise" | "range"
categoryId: Id<"jobCategories">
createdAt: number
jobBids
_id: Id<"jobBids">
jobId: Id<"jobs">
contractorId: Id<"contractorUsers">
amount: number
laborCost, materialCost: number
coverLetter: string
estimatedDays: number
status: BidStatus
attachPortfolio: boolean
contractorProfile
serviceCategories: string[]
serviceRadius: number
latitude, longitude: number
api.jobs.jobs.listAvailable
args: { limit?: number }
returns: { items: Job[] }
// Open jobs, ordered by createdAt desc
api.contractor.jobs.getNearbyJobs
args: {
latitude?: number,
longitude?: number,
radiusMiles: number,
limit: number
}
returns: { items: Job[] }
api.contractor.jobs.getMyBids
args: { status?: BidStatus }
returns: BidWithJob[]
api.contractor.jobs.getSavedJobs
returns: SavedJob[]
api.users.contractors.me
returns: ContractorProfile
api.jobs.bids.create
args: {
jobId: Id<"jobs">,
amount: number,
laborCost: number,
materialCost: number,
coverLetter: string,
estimatedDays: number,
attachPortfolio: boolean
}
api.contractor.jobs.toggleSave
args: { jobId: Id<"jobs"> }
// Bookmark / unbookmark a job
// Hooks
useFindJobs
filterCategory: FilterCategory
searchQuery: string
deviceLocation: { lat, lon } | null
useRefreshControl
returns: [refreshing, onRefresh]
// Components
JobCard // components/features/JobCard
variant: "default" | "compact" | "detailed"
showBidButton: boolean
BidModeBadge // RANGE | PRECISE
StatusBadge // Pending/Accepted/etc.
EmptyState // components/ui/Empty
SearchBar // with filter icon
// Navigation
/(contractor-tabs)/find-jobs
/(contractor)/jobs/[id]
/(contractor)/bids/create