The problem
You cannot quote upholstery from a description. “The seats in my boat are cracked” could mean a two-hour vinyl patch or a full helm rebuild, and the shop finds out only after a phone call, a drive and a look. Every one of those round trips is unpaid time.
What we built
We made the photo the first step instead of the last. The estimate form is not on a contact page — it is the hero, beside the headline, with a drop zone that takes up to ten photos per enquiry. The visitor picks a service, describes the job in a sentence, and attaches pictures of the actual seats or panels.
The services list underneath is written the way the shop talks: seat and cushion reupholstery, custom helm and captain seating, marine vinyl replacement, flooring and carpet, bimini tops and enclosures, boat and console covers. No invented category names.
How we shipped it
Laravel, Inertia and React on Postgres, with photos going presigned direct to S3 so a ten-image upload from a phone on a dock never travels through the application server. One codebase serves both the auto-and-marine and boat-only domains behind a single environment flag.
Outcome
The shop gets enquiries it can actually price, with pictures attached, before anybody drives anywhere. The estimate that used to take a phone call and a site visit now starts as a complete submission.



