# Rentals — playbook
The rental machinery ships as files — the rental catalog (Bookings services carrying the Wix
Rentals app id, paged), the two availability flows (hourly: a start, then the lengths that start
allows; daily: a start day, then the consecutive days it can run into), the server-priced quote for
a start and length, the schema-driven booking form, and the exact `createBooking → cart →
checkout-or-place` sequence with the rental's resource and the customer's end, typed end-to-end.
**The presentation is yours**: you design and implement the rental card, the listing surface, and
the rental surface on the shipped hooks/DTOs, plus the home page and the brand. You never write
rental logic; you never skip designing.
Wix Rentals has no API of its own: a rental is a Wix Bookings service with five field values (the
Rentals app id, a resource type and its resources, the Rentals form, a duration range). The shipped
code knows this so you don't have to; neither the bookings vertical nor the Wix Bookings app is
needed alongside this one, and a site that runs both keeps them apart by the app id.
## The file map (deployed into `src/`)
**On Astro and React the shipped files are tested and work as they are** — this table and the
contracts below are everything you need to use them, so don't spend the run reading their source;
wire them and build your surfaces. Reading them is the right move when something is off (a runtime
error, a field this playbook doesn't cover) or when the brief wants a behaviour they don't offer —
then read the file that owns it and change or extend it. On `lib`, `static`, and a port the
components don't deploy at all, and each wiring section below opens with the files to read before
writing their equivalents. Files you edit: `SiteLayout.astro`, `styles/global.css`, and the two
pages' island imports. Files you **create**: your listing and rental components, plus your home page.
| file | what it is |
|---|---|
| `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
| `wix/media.ts` · `wix/money.ts` | `imgAttrs(url, sizes)` — every `` attribute for a DTO image: ``; `imgSrc()` / `imgSrcSet()` / `formatMoney()` underneath |
| `wix/rentals/types.ts` | the DTOs (`RentalSummary`, `RentalDetail`, `RentalLocation`, `StartOption`, `EndOption`, `RentalQuote`, `RentalFormField`, `RentalResult`) — contracts below |
| `wix/rentals/rentals.ts` | `fetchRentals` (one page), `fetchRentalBySlug`, `fetchStarts`, `fetchEndOptions`, `fetchQuote`, `fetchRentalForm`, `rentResource` — the transport; the rules and DTO mappers (the five rentals values, rate and range labels, the request bodies, the daily walk, checkout-or-place) are in `rentals-core.ts` beside it (shared with the REST layer) |
| `wix/rentals/rentals-store.ts` · `rental-flow-store.ts` | the listing and rental state machines, framework-free (`createRentalsStore()`, `createRentalFlowStore(rental)` — `getState`/`subscribe` + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
| `hooks/rentals/useRentals.ts` | React binding of `rentals-store.ts`: listing + paging — contract below |
| `hooks/rentals/useRentalFlow.ts` | React binding of `rental-flow-store.ts`: the whole rental state machine — contract below |
| `components/rentals/RentalsView.tsx` (+ `RentalCard`) · `RentalBookingView.tsx` | **REFERENCE implementations** — correct, plain; build your own instead of shipping them |
| `styles/global.css` | the design system: Tailwind v4 + the `@theme` token block (shared across verticals) |
Astro stack additionally gets:
| file | what it is |
|---|---|
| `layouts/SiteLayout.astro` | site chrome — **yours to brand** (keep the `seo-tags` slot + global.css import). If another vertical is also deployed, its layout won — add a Rentals nav link there |
| `pages/rentals.astro` | SSR listing — **keep the frontmatter**, swap the island import to YOUR component |
| `pages/rentals/[slug].astro` | SSR detail + rental with owner-editable SEO — **keep the frontmatter and the SEO pieces** (`wixMetadata`, `loadSEOTagsServiceConfig`, ``) exactly; swap the island import. The rental island stays `client:only="react"` (availability is time-zone-specific) |
## What you build — the design job
1. **The rental card + listing surface** — your tile (image, "By the hour" / "By the day", the
rate and range presentation) and rhythm, with skeletons while loading and an honest empty
state — on `useRentals`.
2. **The rental surface** — the start picker one day at a time (a strip of the days that have
availability, then that day's times bucketed into morning, afternoon and evening; a daily rental
shows the strip alone) with window paging at the strip's ends, the length picker for the chosen
start, the quote, the schema-driven form, the CTA labelled from `ctaState`, and the confirmed
state — on `useRentalFlow`, which owns ALL rental logic; you own how it looks.
3. **The home page** — hero, featured rentals (fetch in frontmatter → your components), brand story.
Plus the **theme** (`@theme` block, one edit) and the **chrome** (`SiteLayout`, one pass).
Style everything with Tailwind utilities on the tokens.
### What a complete rentals site shows (recommended defaults)
Defaults for a brief that says nothing about them; the prompt wins where it differs. Look at the
rentals before designing (hourly vs daily, how many units, rates, images) and design for this
business, not a stereotype of its category. Then, by default:
- **Home:** what is for rent and one rental action in the first screen; real rentals under truthful
headings; not a repeat of the listing.
- **Listing:** a real rental card — image, name, unit badge, `rateLabel` ("$40 / hour") and
`rangeLabel` ("1 to 8 hours"), link — in the first screen; nothing when a label is ""; a
"Load more" affordance while `hasMore`; `rentals === null` → skeleton tiles, `[]` → your honest
empty state.
- **Rental page:** name, rate, and the first available day's starts in the first screen **at 390px
wide too** — the image is a bounded band on a phone, not a full-screen hero. **One day at a
time:** a horizontal strip of the days in `days` (weekday and day number), the active day's
starts under it bucketed into morning / afternoon / evening, and the window pagers at the strip's
ends (`windowDays` is 7 for hourly, 14 for daily). Never list every day's times at once: a
rental's resources are usually bookable around the clock, so a week is over three hundred chips
and the form ends up below a wall of numbers. For a daily rental the strip is the start picker (a
day is a start). Switching day drops a start chosen on another day. "Times in {timeZone}" for an
hourly rental once known; a start chip keyed by `start.key`, disabled when `!bookable`; a window
with no starts says so and offers the next one; once a start is chosen, the length picker from `endOptions`
(`null` → loading, `[]` → "this start can't be rented for the minimum length — pick another");
once a length is chosen, the quote (`quote === null` → "calculating", `quote.total` → the total,
"" → omit); the form under the pickers, the CTA under the form.
- **CTA label:** from `ctaState` — "Rent" / "Request to rent" — then the money: "— free" when
`rental.free`, "· {quote.total}" once quoted, else "· {rateLabel}"; nothing when both are "".
- **Confirmed state:** rendered only from `confirmed`; a visitor returning from the hosted checkout
is not a success signal. Its copy follows `ctaState`: "You're all set" + "a confirmation email is
on its way" for `rent`; "Request sent" + "you'll hear back once it's approved" for
`requestToRent` — never promise a confirmed rental for a request.
- **A slug that resolves to nothing** shows only the not-found state.
- **Copy:** nothing the owner didn't supply — no invented reviews, availability pressure, or
guarantees; no Wix IDs or technical words in visible text.
### The contracts your components consume
Everything you need to build on the shipped code; read the source only when something is off.
```ts
// RentalSummary (tiles) — display-ready, every money value already a string ("" = unknown, omit it):
// { id, slug, name, tagLine, unit: "HOUR"|"DAY",
// ratePerUnit /* "$40" | "Free" | "" */, rateLabel /* "$40 / hour" | "$40 / day" | "Free" | "" */,
// rateAmount, currency /* numbers for the store's fallback math — never render these */,
// minUnits, maxUnits /* in the unit; hours may be 0.5 */, rangeLabel /* "1 to 8 hours" | "3 days" | "" */,
// imageUrl /* https or "" */, resourceTypeId|null, resourceCount,
// locations: [{ id|null, name, type: "BUSINESS"|"CUSTOM"|"CUSTOMER" }],
// onlineBookingEnabled, requiresManualApproval, free }
// RentalDetail adds: description, formId|null, paymentOption, cancellationFeeEnabled, scheduleId|null.
// useRentals({ initialRentals?, initialHasMore?, pageSize? = 20 }) →
// { rentals: RentalSummary[]|null /* null = loading → skeletons */, hasMore, loadingMore, loadMore(), error }
// useRentalFlow(rental: RentalDetail) →
// { days: [{ dayKey, dayLabel, starts: StartOption[] }] | null, // day-grouped; null = loading; [] = none in the window
// windowStart, windowDays, nextWindow(), prevWindow(), jumpTo(dayKey), // paging on day boundaries (prev clamps to today)
// timeZone|null, // the business zone the times are in (from the response)
// selectedStart, setSelectedStart(start|null), // ignores a !bookable start; clears the length and the quote
// // StartOption = { key, startLocal, dayKey, label /* "9:00 AM" | "Mon, Oct 5" */, bookable, location|null,
// // resource: { id, name } | null, scheduleId|null }
// endOptions: EndOption[] | null, selectedEnd, setSelectedEnd(end|null), // null = loading (or no start yet); [] = none
// // EndOption = { endLocal, units, label /* "2 hours · until 11:00 AM" | "3 days · until Wed, Oct 7" */ }
// quote: { total, totalAmount, units, unit } | null, // the server's price for start + length; null until both chosen and priced
// formFields: [{ target, label, type, options?, required }], // never empty (contact-basics fallback); only required ones gate canRent
// values, setValue(target, value), // inputs write here, keyed by target
// ctaState: "rent"|"requestToRent", canRent, // label the CTA from ctaState; gate it on canRent
// rent(): Promise, // paid → the browser navigates to the Wix checkout;
// renting, confirmed, error } // free/offline → confirmed is set (REAL success)
// A window change clears selectedStart, selectedEnd and quote and sets days to null while the new window loads.
// rent() rejects on refusal (start taken, length refused, invalid form) AND records .error — render it beside the CTA.
```
### The reference files for stacks where the components don't deploy
On `lib`, `static`, and a port, nothing under `components/` or `hooks/` arrives. The state machines
behind the hooks do arrive — `wix/rentals/rentals-store.ts`, `rental-flow-store.ts` — so you never
rewrite them: create a store per surface, `subscribe`, render from `getState()`, call its actions.
Their `RentalsState` / `RentalFlowState` interfaces are the render contract; read those. What you
write is the rendering — tiles, the start and length pickers, the form, the CTA — and for that read
these first; they are tested code for exactly that behaviour:
1. `components/rentals/RentalBookingView.tsx` — the rental surface as working code: the day strip
(the days with availability, pagers at its ends), the active day's starts in morning / afternoon
/ evening buckets for an hourly rental, the strip as the start picker for a daily one, the
time-zone line,
the length picker with its loading and empty states, the quote line, one input per `formFields`
entry typed from its `type` (a `