diff --git a/src/chatelet-app/App.vue b/src/chatelet-app/App.vue index b98cf17..0b1977e 100644 --- a/src/chatelet-app/App.vue +++ b/src/chatelet-app/App.vue @@ -1,40 +1,13 @@ diff --git a/src/components/ui/range-calendar/RangeCalendar.vue b/src/components/ui/range-calendar/RangeCalendar.vue deleted file mode 100644 index 2d11816..0000000 --- a/src/components/ui/range-calendar/RangeCalendar.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarCell.vue b/src/components/ui/range-calendar/RangeCalendarCell.vue deleted file mode 100644 index e5dea17..0000000 --- a/src/components/ui/range-calendar/RangeCalendarCell.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue b/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue deleted file mode 100644 index 7971c55..0000000 --- a/src/components/ui/range-calendar/RangeCalendarCellTrigger.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarGrid.vue b/src/components/ui/range-calendar/RangeCalendarGrid.vue deleted file mode 100644 index c1db6c9..0000000 --- a/src/components/ui/range-calendar/RangeCalendarGrid.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarGridBody.vue b/src/components/ui/range-calendar/RangeCalendarGridBody.vue deleted file mode 100644 index 68556dd..0000000 --- a/src/components/ui/range-calendar/RangeCalendarGridBody.vue +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarGridHead.vue b/src/components/ui/range-calendar/RangeCalendarGridHead.vue deleted file mode 100644 index f6cc09e..0000000 --- a/src/components/ui/range-calendar/RangeCalendarGridHead.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarGridRow.vue b/src/components/ui/range-calendar/RangeCalendarGridRow.vue deleted file mode 100644 index 717396c..0000000 --- a/src/components/ui/range-calendar/RangeCalendarGridRow.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarHeadCell.vue b/src/components/ui/range-calendar/RangeCalendarHeadCell.vue deleted file mode 100644 index 7ea377f..0000000 --- a/src/components/ui/range-calendar/RangeCalendarHeadCell.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarHeader.vue b/src/components/ui/range-calendar/RangeCalendarHeader.vue deleted file mode 100644 index 5cef2d5..0000000 --- a/src/components/ui/range-calendar/RangeCalendarHeader.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarHeading.vue b/src/components/ui/range-calendar/RangeCalendarHeading.vue deleted file mode 100644 index 90845e3..0000000 --- a/src/components/ui/range-calendar/RangeCalendarHeading.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarNextButton.vue b/src/components/ui/range-calendar/RangeCalendarNextButton.vue deleted file mode 100644 index f60c786..0000000 --- a/src/components/ui/range-calendar/RangeCalendarNextButton.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/RangeCalendarPrevButton.vue b/src/components/ui/range-calendar/RangeCalendarPrevButton.vue deleted file mode 100644 index 8ed2032..0000000 --- a/src/components/ui/range-calendar/RangeCalendarPrevButton.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/src/components/ui/range-calendar/index.ts b/src/components/ui/range-calendar/index.ts deleted file mode 100644 index 34d5361..0000000 --- a/src/components/ui/range-calendar/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { default as RangeCalendar } from "./RangeCalendar.vue" -export { default as RangeCalendarCell } from "./RangeCalendarCell.vue" -export { default as RangeCalendarCellTrigger } from "./RangeCalendarCellTrigger.vue" -export { default as RangeCalendarGrid } from "./RangeCalendarGrid.vue" -export { default as RangeCalendarGridBody } from "./RangeCalendarGridBody.vue" -export { default as RangeCalendarGridHead } from "./RangeCalendarGridHead.vue" -export { default as RangeCalendarGridRow } from "./RangeCalendarGridRow.vue" -export { default as RangeCalendarHeadCell } from "./RangeCalendarHeadCell.vue" -export { default as RangeCalendarHeader } from "./RangeCalendarHeader.vue" -export { default as RangeCalendarHeading } from "./RangeCalendarHeading.vue" -export { default as RangeCalendarNextButton } from "./RangeCalendarNextButton.vue" -export { default as RangeCalendarPrevButton } from "./RangeCalendarPrevButton.vue" diff --git a/src/modules/chatelet/components/BookRoomDialog.vue b/src/modules/chatelet/components/BookRoomDialog.vue index e928484..61381ff 100644 --- a/src/modules/chatelet/components/BookRoomDialog.vue +++ b/src/modules/chatelet/components/BookRoomDialog.vue @@ -1,6 +1,5 @@ - - diff --git a/src/modules/chatelet/composables/useMyBookings.ts b/src/modules/chatelet/composables/useMyBookings.ts deleted file mode 100644 index fe6682e..0000000 --- a/src/modules/chatelet/composables/useMyBookings.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { computed } from 'vue' -import { useAsyncState } from '@vueuse/core' -import { getLocalTimeZone, today } from '@internationalized/date' -import { useAuth } from '@/composables/useAuthService' -import { injectService, SERVICE_TOKENS, tryInjectService } from '@/core/di-container' -import type { ChateletApiService } from '../services/ChateletApiService' -import { LOST_BOOKING_STATUSES, type GuestBooking } from '../types/booking' -import { useChateletStore } from '../stores/chatelet' - -/** - * The signed-in guest's stays, split into upcoming and past. Room titles are - * joined client-side from the public room list (a handful of rooms per - * install); a room that has since gone inactive falls back to its id. - */ -export function useMyBookings() { - const { isAuthenticated, currentUser } = useAuth() - const api = tryInjectService(SERVICE_TOKENS.CHATELET_API) - const store = useChateletStore() - - const { - state: bookings, - isLoading, - error: asyncError, - execute: refresh, - } = useAsyncState( - async () => { - if (!api || !isAuthenticated.value || !currentUser.value) return [] - const lnbitsAPI = injectService(SERVICE_TOKENS.LNBITS_API) as any - const accessToken: string = lnbitsAPI?.getAccessToken?.() || '' - return await api.getMyBookings(accessToken) - }, - [] as GuestBooking[], - { immediate: false, resetOnExecute: false }, - ) - - const error = computed(() => - asyncError.value - ? asyncError.value instanceof Error - ? asyncError.value.message - : 'Could not load your bookings' - : null, - ) - - const todayYmd = today(getLocalTimeZone()).toString() - - /** Still relevant: not over yet and the hold/booking is alive. */ - const upcoming = computed(() => - bookings.value - .filter((b) => b.check_out > todayYmd && !LOST_BOOKING_STATUSES.has(b.status)) - .sort((a, b) => a.check_in.localeCompare(b.check_in)), - ) - const past = computed(() => - bookings.value - .filter((b) => b.check_out <= todayYmd || LOST_BOOKING_STATUSES.has(b.status)) - .sort((a, b) => b.check_in.localeCompare(a.check_in)), - ) - - function roomTitle(roomId: string): string { - return store.rooms.find((r) => r.id === roomId)?.title ?? `Room ${roomId}` - } - function roomImage(roomId: string): string | undefined { - return store.rooms.find((r) => r.id === roomId)?.images?.[0] - } - - return { bookings, upcoming, past, isLoading, error, refresh, roomTitle, roomImage } -} diff --git a/src/modules/chatelet/composables/useRoomUnavailability.ts b/src/modules/chatelet/composables/useRoomUnavailability.ts deleted file mode 100644 index 684b931..0000000 --- a/src/modules/chatelet/composables/useRoomUnavailability.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { computed, ref, watch, type Ref } from 'vue' -import { getLocalTimeZone, today } from '@internationalized/date' -import { SERVICE_TOKENS, tryInjectService } from '@/core/di-container' -import type { ChateletApiService } from '../services/ChateletApiService' -import { addDays, expandRanges } from '../lib/unavailable' - -const WINDOW_DAYS = 365 - -/** - * The occupied nights of one room for the calendar. One fetch of a 12-month - * window per room — a few spans, cheap — and the calendar's `maxValue` is - * pinned to the window end, so no refetch on month navigation. `refresh()` - * after a booking lands, or when a quote comes back "not available" for a - * stay the local set thought free (someone else's hold appeared). - */ -export function useRoomUnavailability(roomId: Ref) { - // DI, never a direct import (workspace rule). - const api = tryInjectService(SERVICE_TOKENS.CHATELET_API) - - const nights = ref([]) - const windowStart = ref(today(getLocalTimeZone()).toString()) - const windowEnd = ref(addDays(windowStart.value, WINDOW_DAYS)) - const isLoading = ref(false) - const error = ref(null) - - const nightSet = computed(() => new Set(nights.value)) - - async function refresh(): Promise { - if (!api || !roomId.value) return - isLoading.value = true - error.value = null - try { - const start = today(getLocalTimeZone()).toString() - const res = await api.getUnavailableRanges(roomId.value, start, addDays(start, WINDOW_DAYS)) - windowStart.value = res.start - windowEnd.value = res.end - nights.value = expandRanges(res.ranges) - } catch (e: unknown) { - error.value = e instanceof Error ? e.message : 'Could not load availability' - } finally { - isLoading.value = false - } - } - - watch(roomId, () => void refresh(), { immediate: true }) - - return { nights, nightSet, windowStart, windowEnd, isLoading, error, refresh } -} diff --git a/src/modules/chatelet/index.ts b/src/modules/chatelet/index.ts index f71e445..8980b69 100644 --- a/src/modules/chatelet/index.ts +++ b/src/modules/chatelet/index.ts @@ -31,14 +31,6 @@ export const chateletModule = createModulePlugin({ component: () => import('./views/ChateletDetailPage.vue'), meta: { title: 'Room', requiresAuth: false }, }, - { - // Mirrors events' /my-tickets; kept off /chatelet/* so it never - // collides with the :id room route. - path: '/my-bookings', - name: 'chatelet-my-bookings', - component: () => import('./views/MyBookingsPage.vue'), - meta: { title: 'My bookings', requiresAuth: true }, - }, ], onInstall: async (_app, options) => { @@ -60,4 +52,4 @@ export const chateletModule = createModulePlugin({ export default chateletModule export type { Room, AvailabilityResult } from './types/room' -export type { Booking, BookingQuote, BookingRequest, BookingStatus, GuestBooking } from './types/booking' +export type { Booking, BookingQuote, BookingRequest, BookingStatus } from './types/booking' diff --git a/src/modules/chatelet/lib/unavailable.spec.ts b/src/modules/chatelet/lib/unavailable.spec.ts deleted file mode 100644 index 2756ce6..0000000 --- a/src/modules/chatelet/lib/unavailable.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - addDays, - expandRanges, - nextUnavailableOnOrAfter, - nightsBetween, - stayIsFree, -} from './unavailable' - -describe('unavailable-night helpers', () => { - it('addDays crosses month and year boundaries', () => { - expect(addDays('2026-10-31', 1)).toBe('2026-11-01') - expect(addDays('2026-12-31', 1)).toBe('2027-01-01') - expect(addDays('2026-03-01', -1)).toBe('2026-02-28') - }) - - it('nightsBetween is check_out-exclusive', () => { - expect(nightsBetween('2026-10-05', '2026-10-07')).toBe(2) - expect(nightsBetween('2026-10-05', '2026-10-05')).toBe(0) - }) - - it('expandRanges yields sorted, de-duplicated occupied nights (end exclusive)', () => { - const nights = expandRanges([ - { start: '2026-10-10', end: '2026-10-12' }, - { start: '2026-10-05', end: '2026-10-07' }, - { start: '2026-10-11', end: '2026-10-13' }, - ]) - expect(nights).toEqual(['2026-10-05', '2026-10-06', '2026-10-10', '2026-10-11', '2026-10-12']) - }) - - it('nextUnavailableOnOrAfter finds the boundary night', () => { - const nights = ['2026-10-05', '2026-10-06', '2026-10-10'] - expect(nextUnavailableOnOrAfter(nights, '2026-10-01')).toBe('2026-10-05') - expect(nextUnavailableOnOrAfter(nights, '2026-10-05')).toBe('2026-10-05') - expect(nextUnavailableOnOrAfter(nights, '2026-10-07')).toBe('2026-10-10') - expect(nextUnavailableOnOrAfter(nights, '2026-10-11')).toBeUndefined() - expect(nextUnavailableOnOrAfter([], '2026-10-11')).toBeUndefined() - }) - - it('stayIsFree honours half-open semantics', () => { - const set = new Set(['2026-10-05', '2026-10-06']) - // checking out the morning the other guest checks in is fine - expect(stayIsFree(set, '2026-10-03', '2026-10-05')).toBe(true) - // checking in the morning they leave is fine too - expect(stayIsFree(set, '2026-10-07', '2026-10-09')).toBe(true) - // spanning an occupied night is not - expect(stayIsFree(set, '2026-10-04', '2026-10-06')).toBe(false) - expect(stayIsFree(set, '2026-10-06', '2026-10-06')).toBe(false) - }) -}) diff --git a/src/modules/chatelet/lib/unavailable.ts b/src/modules/chatelet/lib/unavailable.ts deleted file mode 100644 index 0904b4c..0000000 --- a/src/modules/chatelet/lib/unavailable.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { DateRange } from '../types/room' - -/** YYYY-MM-DD → YYYY-MM-DD, `days` later. Date-only math, no timezone. */ -export function addDays(ymd: string, days: number): string { - const d = new Date(ymd + 'T00:00:00Z') - d.setUTCDate(d.getUTCDate() + days) - return d.toISOString().slice(0, 10) -} - -/** Nights between two YYYY-MM-DD dates (check_out exclusive). */ -export function nightsBetween(checkIn: string, checkOut: string): number { - const a = Date.UTC(+checkIn.slice(0, 4), +checkIn.slice(5, 7) - 1, +checkIn.slice(8, 10)) - const b = Date.UTC(+checkOut.slice(0, 4), +checkOut.slice(5, 7) - 1, +checkOut.slice(8, 10)) - return Math.round((b - a) / 86_400_000) -} - -/** - * Expand half-open [start, end) spans into the sorted, de-duplicated list of - * occupied nights. A year of a busy room is a few hundred strings — cheap to - * hold and to binary-search. - */ -export function expandRanges(ranges: DateRange[]): string[] { - const out = new Set() - for (const r of ranges) { - for (let d = r.start; d < r.end; d = addDays(d, 1)) out.add(d) - } - return [...out].sort() -} - -/** First occupied night ≥ `ymd`, or undefined if none in the loaded window. */ -export function nextUnavailableOnOrAfter(nights: string[], ymd: string): string | undefined { - let lo = 0 - let hi = nights.length - while (lo < hi) { - const mid = (lo + hi) >> 1 - if (nights[mid] < ymd) lo = mid + 1 - else hi = mid - } - return nights[lo] -} - -/** True when every night of [checkIn, checkOut) is free. */ -export function stayIsFree(nightSet: Set, checkIn: string, checkOut: string): boolean { - if (!(checkIn < checkOut)) return false - for (let d = checkIn; d < checkOut; d = addDays(d, 1)) { - if (nightSet.has(d)) return false - } - return true -} diff --git a/src/modules/chatelet/services/ChateletApiService.ts b/src/modules/chatelet/services/ChateletApiService.ts index 7378157..2be1400 100644 --- a/src/modules/chatelet/services/ChateletApiService.ts +++ b/src/modules/chatelet/services/ChateletApiService.ts @@ -1,5 +1,5 @@ -import type { AvailabilityResult, Room, UnavailableRanges } from '../types/room' -import type { BookingQuote, BookingRequest, GuestBooking, PublicBooking } from '../types/booking' +import type { AvailabilityResult, Room } from '../types/room' +import type { BookingQuote, BookingRequest, PublicBooking } from '../types/booking' export interface ChateletApiConfig { baseUrl: string @@ -30,26 +30,6 @@ export class ChateletApiService { return this.request(`/chatelet/api/v1/public/rooms/${id}`, { method: 'GET' }) } - /** - * Nights a guest cannot book over a window (default today → +365 d): live - * holds, confirmed stays and manual blocks merged into anonymous spans. - * Keyless, like the room read it hangs off. - */ - async getUnavailableRanges( - roomId: string, - start?: string, - end?: string, - ): Promise { - const q = new URLSearchParams() - if (start) q.set('start', start) - if (end) q.set('end', end) - const qs = q.toString() - return this.request( - `/chatelet/api/v1/public/rooms/${roomId}/unavailable${qs ? `?${qs}` : ''}`, - { method: 'GET' }, - ) - } - async checkAvailability( roomId: string, checkIn: string, @@ -84,18 +64,6 @@ export class ChateletApiService { return this.request(`/chatelet/api/v1/public/bookings/${bookingId}`, { method: 'GET' }) } - /** - * The signed-in guest's own stays, newest check-in first. LNbits account - * auth (Bearer), the same way events fetches a user's tickets; the backend - * matches on the account's Nostr pubkey. - */ - async getMyBookings(accessToken: string): Promise { - return this.request('/chatelet/api/v1/bookings/mine', { - method: 'GET', - headers: { Authorization: `Bearer ${accessToken}` }, - }) - } - private async request(path: string, init: RequestInit = {}): Promise { const headers: Record = { accept: 'application/json', diff --git a/src/modules/chatelet/types/booking.ts b/src/modules/chatelet/types/booking.ts index 56f685b..83c0831 100644 --- a/src/modules/chatelet/types/booking.ts +++ b/src/modules/chatelet/types/booking.ts @@ -70,13 +70,6 @@ export type PublicBooking = Omit< 'guest_pubkey' | 'guest_contact' | 'payment_hash' > -/** - * A booking as returned to its own guest by `GET /chatelet/api/v1/bookings/mine` - * (LNbits account auth, chatelet ≥ v0.5.0): the guest keeps their contact and - * counts; the Lightning/Nostr plumbing stays server-side. - */ -export type GuestBooking = Omit - /** Statuses that mean the hold is gone and the invoice will never confirm. */ export const LOST_BOOKING_STATUSES: ReadonlySet = new Set([ 'expired', diff --git a/src/modules/chatelet/types/room.ts b/src/modules/chatelet/types/room.ts index 7115fa8..6800d6a 100644 --- a/src/modules/chatelet/types/room.ts +++ b/src/modules/chatelet/types/room.ts @@ -2,13 +2,6 @@ // Note: operator-private fields (wallet, checkin_instructions) are stripped // server-side by public_room_dict and never reach the client. -/** The room owner's rules, from their operator settings (chatelet ≥ v0.5.0). */ -export interface HouseRules { - checkin_time: string - checkout_time: string - cancellation_policy: string -} - export interface Room { id: string title: string @@ -26,10 +19,6 @@ export interface Room { listing_event_id: string | null created_at: string updated_at: string - /** Absent on chatelet < v0.5.0. */ - house_rules?: HouseRules - /** Rails the owner accepts for this room: 'lightning' and maybe 'fiat'. Absent on < v0.5.0. */ - payment_methods?: string[] } export interface AvailabilityResult { @@ -42,17 +31,3 @@ export interface AvailabilityResult { quote_fiat: number | null currency: string | null } - -/** Half-open [start, end) span of occupied nights; `end` is the morning the room frees up. */ -export interface DateRange { - start: string - end: string -} - -/** `GET /chatelet/api/v1/public/rooms/{id}/unavailable` — the guest calendar feed. */ -export interface UnavailableRanges { - room_id: string - start: string - end: string - ranges: DateRange[] -} diff --git a/src/modules/chatelet/views/ChateletDetailPage.vue b/src/modules/chatelet/views/ChateletDetailPage.vue index 612d425..ae43195 100644 --- a/src/modules/chatelet/views/ChateletDetailPage.vue +++ b/src/modules/chatelet/views/ChateletDetailPage.vue @@ -2,19 +2,16 @@ import { computed, onMounted, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { toast } from 'vue-sonner' -import { watchDebounced } from '@vueuse/core' -import { Clock, MapPin, Moon, Users } from 'lucide-vue-next' +import { addDays, format } from 'date-fns' +import { MapPin, Users } from 'lucide-vue-next' import { Card, CardContent } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' -import { ImageViewer } from '@/components/ui/image' -import { formatSats } from '@/lib/utils/formatting' +import { Label } from '@/components/ui/label' +import DatePicker from '@/modules/base/components/DatePicker.vue' import { useAuth } from '@/composables/useAuthService' import { useChatelet } from '../composables/useChatelet' -import { useRoomUnavailability } from '../composables/useRoomUnavailability' -import { stayIsFree } from '../lib/unavailable' import BookRoomDialog from '../components/BookRoomDialog.vue' -import StayDatePicker from '../components/StayDatePicker.vue' import type { AvailabilityResult, Room } from '../types/room' const route = useRoute() @@ -33,23 +30,25 @@ const showBooking = ref(false) * `result` being cleared once the dates are taken. */ const bookingQuote = ref(null) -// Occupied nights for the calendar; refreshed after a booking and when a -// quote contradicts the local set (someone else's hold appeared). -const roomIdRef = computed(() => route.params.id as string | undefined) -const unavailability = useRoomUnavailability(roomIdRef) +// Same YYYY-MM-DD wire format the DatePicker speaks; the availability +// endpoint takes the same strings. +const today = computed(() => format(new Date(), 'yyyy-MM-dd')) -// Any date change invalidates the last quote; a complete stay re-quotes -// itself (the availability call is the price preview, so no button). -watch([checkIn, checkOut], () => { +// Check-out must be at least the next morning. Bumping the floor also +// clears a check-out that the new check-in has overtaken. +const minCheckOut = computed(() => + checkIn.value + ? format(addDays(new Date(checkIn.value + 'T00:00:00'), 1), 'yyyy-MM-dd') + : today.value, +) + +watch(checkIn, () => { + if (checkOut.value && checkOut.value < minCheckOut.value) checkOut.value = '' + result.value = null +}) +watch(checkOut, () => { result.value = null }) -watchDebounced( - [checkIn, checkOut], - ([ci, co]) => { - if (ci && co && room.value) void onCheck() - }, - { debounce: 150 }, -) const YMD = /^\d{4}-\d{2}-\d{2}$/ @@ -67,6 +66,7 @@ onMounted(async () => { if (typeof qi === 'string' && YMD.test(qi) && typeof qo === 'string' && YMD.test(qo)) { checkIn.value = qi checkOut.value = qo + if (room.value) await onCheck() } }) @@ -75,16 +75,7 @@ async function onCheck() { checking.value = true result.value = null try { - const ci = checkIn.value - const co = checkOut.value - const r = await checkAvailability(room.value.id, ci, co) - // Only the quote for the dates still selected counts (debounce race). - if (ci !== checkIn.value || co !== checkOut.value) return - result.value = r - if (r && !r.available && stayIsFree(unavailability.nightSet.value, ci, co)) { - // The calendar thought these nights were free — it is stale. - void unavailability.refresh() - } + result.value = await checkAvailability(room.value.id, checkIn.value, checkOut.value) } catch (e: unknown) { toast.error(e instanceof Error ? e.message : 'Availability check failed') } finally { @@ -111,12 +102,11 @@ function onBook() { showBooking.value = true } -/** The dates are taken now — drop the stale quote and grey them out. */ +/** The dates are taken now — drop the stale quote so the card resets. */ function onBooked() { result.value = null checkIn.value = '' checkOut.value = '' - void unavailability.refresh() } watch(showBooking, (open) => { @@ -130,18 +120,11 @@ watch(showBooking, (open) => {
Room not found.
- -
@@ -169,66 +152,40 @@ watch(showBooking, (open) => { {{ room.description }}

- - - -

Things to know

-
    -
  • - - Check-in from {{ room.house_rules.checkin_time }} · check-out by - {{ room.house_rules.checkout_time }} -
  • -
  • - - Up to {{ room.max_guests }} guest{{ room.max_guests === 1 ? '' : 's' }} -
  • -
  • - - Minimum stay {{ room.min_nights }} night{{ room.min_nights === 1 ? '' : 's' }} -
  • -
-
-

Cancellation policy

-

- {{ room.house_rules.cancellation_policy }} -

-
-
-
- - - -

- Could not load the availability calendar — you can still pick dates and check them. -

- -
-
- Checking availability… + +
+
+ + +
+
+ + +
+ -
+