# 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: `{r.name}`; `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 `