feat(chatelet): stay calendar, room gallery + house rules, My bookings #156
2 changed files with 52 additions and 4 deletions
feat(chatelet): photo gallery and house rules on the room page
Swap the single hero image for the shared ImageViewer (thumbnails, lightbox and cycle controls when a room has more than one photo, as the marketplace product page does) and add a 'Things to know' card: check-in from / check-out by, guest cap, minimum stay and the cancellation policy. The rules come from the room owner's operator settings (chatelet ≥ v0.5.0) so the card only renders when the extension sends them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
commit
fb0478c028
|
|
@ -2,6 +2,13 @@
|
||||||
// Note: operator-private fields (wallet, checkin_instructions) are stripped
|
// Note: operator-private fields (wallet, checkin_instructions) are stripped
|
||||||
// server-side by public_room_dict and never reach the client.
|
// 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 {
|
export interface Room {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
|
@ -19,6 +26,10 @@ export interface Room {
|
||||||
listing_event_id: string | null
|
listing_event_id: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_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 {
|
export interface AvailabilityResult {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { watchDebounced } from '@vueuse/core'
|
import { watchDebounced } from '@vueuse/core'
|
||||||
import { MapPin, Users } from 'lucide-vue-next'
|
import { Clock, MapPin, Moon, Users } from 'lucide-vue-next'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ImageViewer } from '@/components/ui/image'
|
||||||
import { formatSats } from '@/lib/utils/formatting'
|
import { formatSats } from '@/lib/utils/formatting'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { useChatelet } from '../composables/useChatelet'
|
import { useChatelet } from '../composables/useChatelet'
|
||||||
|
|
@ -129,11 +130,18 @@ watch(showBooking, (open) => {
|
||||||
<div v-else-if="!room" class="text-muted-foreground py-12">Room not found.</div>
|
<div v-else-if="!room" class="text-muted-foreground py-12">Room not found.</div>
|
||||||
|
|
||||||
<div v-else class="space-y-4">
|
<div v-else class="space-y-4">
|
||||||
<img
|
<!-- Gallery: thumbnails + lightbox when there is more than one photo
|
||||||
|
(same ImageViewer the marketplace product page uses). -->
|
||||||
|
<ImageViewer
|
||||||
v-if="room.images?.length"
|
v-if="room.images?.length"
|
||||||
:src="room.images[0]"
|
:images="room.images"
|
||||||
:alt="room.title"
|
:alt="room.title"
|
||||||
class="w-full h-56 object-cover rounded-lg"
|
container-class="aspect-[4/3] sm:aspect-video rounded-lg overflow-hidden bg-muted"
|
||||||
|
image-class="w-full h-full object-cover"
|
||||||
|
:show-thumbnails="room.images.length > 1"
|
||||||
|
:show-lightbox="true"
|
||||||
|
:show-badge="room.images.length > 1"
|
||||||
|
:show-cycle-controls="room.images.length > 1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -161,6 +169,35 @@ watch(showBooking, (open) => {
|
||||||
{{ room.description }}
|
{{ room.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- House rules come from the room owner's operator settings; older
|
||||||
|
extensions don't send them, so the card is conditional. -->
|
||||||
|
<Card v-if="room.house_rules">
|
||||||
|
<CardContent class="p-4 space-y-2">
|
||||||
|
<h2 class="font-semibold text-foreground">Things to know</h2>
|
||||||
|
<ul class="text-sm text-muted-foreground space-y-1">
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<Clock class="h-4 w-4 shrink-0" />
|
||||||
|
Check-in from {{ room.house_rules.checkin_time }} · check-out by
|
||||||
|
{{ room.house_rules.checkout_time }}
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<Users class="h-4 w-4 shrink-0" />
|
||||||
|
Up to {{ room.max_guests }} guest{{ room.max_guests === 1 ? '' : 's' }}
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center gap-2">
|
||||||
|
<Moon class="h-4 w-4 shrink-0" />
|
||||||
|
Minimum stay {{ room.min_nights }} night{{ room.min_nights === 1 ? '' : 's' }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-if="room.house_rules.cancellation_policy" class="pt-1">
|
||||||
|
<p class="text-sm font-medium text-foreground">Cancellation policy</p>
|
||||||
|
<p class="text-sm text-muted-foreground whitespace-pre-line">
|
||||||
|
{{ room.house_rules.cancellation_policy }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent class="p-4 space-y-4">
|
<CardContent class="p-4 space-y-4">
|
||||||
<StayDatePicker
|
<StayDatePicker
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue