Merged in fix/STAY-65-manage-stay (pull request #3089)
Fix/STAY-65 manage stay * fix: Disable manage stay for past bookings * fix: handle past and cancelled stay the same * fix: indentify past booking * fix: refactor to use design system components Approved-by: Erik Tiekstra
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
div a.link {
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--Component-Button-Brand-Tertiary-Fill-Default);
|
|
||||||
border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default);
|
|
||||||
border-radius: var(--Corner-radius-rounded);
|
|
||||||
color: var(--Text-Inverted);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Space-x1);
|
|
||||||
height: 48px;
|
|
||||||
justify-content: center;
|
|
||||||
padding: var(--Space-x2) var(--Space-x4);
|
|
||||||
transition: background-color 200ms ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import { usePathname } from "next/navigation"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
|
||||||
|
|
||||||
import { isWebview } from "@/constants/routes/webviews"
|
|
||||||
|
|
||||||
import CustomerSupport from "./CustomerSupport"
|
|
||||||
|
|
||||||
import styles from "./cancelled.module.css"
|
|
||||||
|
|
||||||
export default function Cancelled() {
|
|
||||||
const intl = useIntl()
|
|
||||||
const pathname = usePathname()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* (S) TODO - Link to where?? */}
|
|
||||||
{!isWebview(pathname) && (
|
|
||||||
<Link className={styles.link} href="#">
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Rebook",
|
|
||||||
id: "myStay.referenceCard.actions.rebook",
|
|
||||||
})}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CustomerSupport />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
.trigger {
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--Component-Button-Brand-Tertiary-Fill-Default);
|
|
||||||
border: 2px solid var(--Component-Button-Brand-Tertiary-Border-Default);
|
|
||||||
border-radius: var(--Corner-radius-rounded);
|
|
||||||
color: var(--Text-Inverted);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Space-x1);
|
|
||||||
height: 48px;
|
|
||||||
justify-content: center;
|
|
||||||
padding: var(--Space-x2) var(--Space-x4);
|
|
||||||
transition: background-color 200ms ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--Component-Button-Brand-Tertiary-Fill-Hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
background-color: var(--Component-Button-Brand-Tertiary-Fill-Disabled);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
display: grid;
|
|
||||||
flex: 1;
|
|
||||||
gap: var(--Space-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: var(--Space-x2);
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: var(--Text-Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Space-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.dialog {
|
|
||||||
gap: var(--Space-x3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
gap: var(--Space-x3);
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"use client"
|
|
||||||
import Link from "next/link"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
|
||||||
|
|
||||||
import ManageStay from "./ManageStay"
|
|
||||||
|
|
||||||
import styles from "./notCancelled.module.css"
|
|
||||||
|
|
||||||
export default function NotCancelled() {
|
|
||||||
const intl = useIntl()
|
|
||||||
const { hotel } = useMyStayStore((state) => state)
|
|
||||||
|
|
||||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(
|
|
||||||
`${hotel.name}, ${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`
|
|
||||||
)}`
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ManageStay />
|
|
||||||
<Link className={styles.link} href={directionsUrl} target="_blank">
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "myStay.referenceCard.actions.findUs",
|
|
||||||
defaultMessage: "Find us",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
<MaterialIcon color="Icon/Interactive/Default" icon="location_on" />
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.link {
|
|
||||||
align-items: center;
|
|
||||||
border: 2px solid var(--Component-Button-Brand-Secondary-Border-Default);
|
|
||||||
border-radius: var(--Corner-radius-rounded);
|
|
||||||
color: var(--Text-Interactive-Default);
|
|
||||||
display: flex;
|
|
||||||
height: 48px;
|
|
||||||
justify-content: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { DialogTrigger } from "react-aria-components"
|
import { DialogTrigger } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
|
||||||
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
|
||||||
|
|
||||||
@@ -11,7 +11,11 @@ export default function CustomerSupport() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button fullWidth intent="secondary" size="small">
|
<Button
|
||||||
|
variant="Secondary"
|
||||||
|
size="Small"
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "common.customerSupport",
|
id: "common.customerSupport",
|
||||||
defaultMessage: "Customer support",
|
defaultMessage: "Customer support",
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { serializeBookingSearchParams } from "@scandic-hotels/booking-flow/utils/url"
|
||||||
|
import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType"
|
||||||
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
|
|
||||||
|
import { isWebview } from "@/constants/routes/webviews"
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import CustomerSupport from "./CustomerSupport"
|
||||||
|
|
||||||
|
import type { BookingWidgetSearchData } from "@scandic-hotels/booking-flow/BookingWidget"
|
||||||
|
|
||||||
|
export default function NotUpcoming() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const { rooms, mainRoom } = useMyStayStore((state) => ({
|
||||||
|
rooms: state.rooms,
|
||||||
|
mainRoom: state.mainRoom,
|
||||||
|
}))
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
|
const bookingData: BookingWidgetSearchData = {
|
||||||
|
hotelId: mainRoom.hotelId,
|
||||||
|
rooms: [mainRoom, ...rooms].map((room) => ({
|
||||||
|
adults: room.adults,
|
||||||
|
childrenInRoom: room.childrenInRoom,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainRoom.bookingCode) {
|
||||||
|
bookingData.bookingCode = mainRoom.bookingCode
|
||||||
|
}
|
||||||
|
if (mainRoom.bookingType === RateTypeEnum.Redemption) {
|
||||||
|
bookingData.searchType = "redemption"
|
||||||
|
}
|
||||||
|
|
||||||
|
const rebookUrl = serializeBookingSearchParams(bookingData)
|
||||||
|
|
||||||
|
const url = `/${lang}?${rebookUrl}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isWebview(pathname) && (
|
||||||
|
<ButtonLink
|
||||||
|
variant="Tertiary"
|
||||||
|
size="Medium"
|
||||||
|
href={url}
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Rebook",
|
||||||
|
id: "myStay.referenceCard.actions.rebook",
|
||||||
|
})}
|
||||||
|
</ButtonLink>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CustomerSupport />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import {
|
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||||
Button as ButtonRAC,
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "react-aria-components"
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
import { IconButton } from "@scandic-hotels/design-system/IconButton"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
|
||||||
|
|
||||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||||
|
|
||||||
import Actions from "./Actions"
|
import Actions from "./Actions"
|
||||||
@@ -20,13 +16,6 @@ import styles from "./manageStay.module.css"
|
|||||||
|
|
||||||
export default function ManageStay() {
|
export default function ManageStay() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const allRoomsAreCancelled = useMyStayStore(
|
|
||||||
(state) => state.allRoomsAreCancelled
|
|
||||||
)
|
|
||||||
|
|
||||||
const color = allRoomsAreCancelled
|
|
||||||
? "Icon/Interactive/Disabled"
|
|
||||||
: "Icon/Inverted"
|
|
||||||
|
|
||||||
const manageStay = intl.formatMessage({
|
const manageStay = intl.formatMessage({
|
||||||
id: "myStay.manageStay.manageStay",
|
id: "myStay.manageStay.manageStay",
|
||||||
@@ -35,12 +24,14 @@ export default function ManageStay() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<ButtonRAC className={styles.trigger} isDisabled={allRoomsAreCancelled}>
|
<Button
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
size="Medium"
|
||||||
|
variant="Tertiary"
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
>
|
||||||
<span>{manageStay}</span>
|
<span>{manageStay}</span>
|
||||||
</Typography>
|
<MaterialIcon color="CurrentColor" icon="keyboard_arrow_down" />
|
||||||
<MaterialIcon color={color} icon="keyboard_arrow_down" />
|
</Button>
|
||||||
</ButtonRAC>
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog className={styles.dialog}>
|
<Dialog className={styles.dialog}>
|
||||||
{({ close }) => (
|
{({ close }) => (
|
||||||
@@ -49,9 +40,9 @@ export default function ManageStay() {
|
|||||||
<Typography variant="Title/Subtitle/lg">
|
<Typography variant="Title/Subtitle/lg">
|
||||||
<span className={styles.title}>{manageStay}</span>
|
<span className={styles.title}>{manageStay}</span>
|
||||||
</Typography>
|
</Typography>
|
||||||
<ButtonRAC className={styles.close} onPress={close}>
|
<IconButton onPress={close} theme="Inverted">
|
||||||
<MaterialIcon color="Icon/Feedback/Neutral" icon="close" />
|
<MaterialIcon color="CurrentColor" icon="close" />
|
||||||
</ButtonRAC>
|
</IconButton>
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Actions onClose={close} />
|
<Actions onClose={close} />
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.dialog {
|
||||||
|
display: grid;
|
||||||
|
flex: 1;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.dialog {
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
gap: var(--Space-x3);
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import ManageStay from "./ManageStay"
|
||||||
|
|
||||||
|
export default function Upcoming() {
|
||||||
|
const intl = useIntl()
|
||||||
|
const hotel = useMyStayStore((state) => state.hotel)
|
||||||
|
|
||||||
|
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(
|
||||||
|
`${hotel.name}, ${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`
|
||||||
|
)}`
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ManageStay />
|
||||||
|
<ButtonLink
|
||||||
|
variant="Secondary"
|
||||||
|
size="Medium"
|
||||||
|
target="_blank"
|
||||||
|
href={directionsUrl}
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "myStay.referenceCard.actions.findUs",
|
||||||
|
defaultMessage: "Find us",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<MaterialIcon color="CurrentColor" icon="location_on" />
|
||||||
|
</ButtonLink>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
import Cancelled from "./Cancelled"
|
import NotUpcoming from "./NotUpcoming"
|
||||||
import NotCancelled from "./NotCancelled"
|
import Upcoming from "./Upcoming"
|
||||||
|
|
||||||
import styles from "./actions.module.css"
|
import styles from "./actions.module.css"
|
||||||
|
|
||||||
export default function Actions() {
|
export default function Actions() {
|
||||||
const isCancelled = useMyStayStore((state) => state.bookedRoom.isCancelled)
|
const isCancelled = useMyStayStore((state) => state.bookedRoom.isCancelled)
|
||||||
|
const isPastBooking = useMyStayStore((state) => state.isPastBooking)
|
||||||
|
|
||||||
|
const isActionable = !isCancelled && !isPastBooking
|
||||||
return (
|
return (
|
||||||
<div className={styles.actionArea}>
|
<div className={styles.actionArea}>
|
||||||
{isCancelled ? <Cancelled /> : <NotCancelled />}
|
{isActionable ? <Upcoming /> : <NotUpcoming />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
|
|||||||
import { use, useRef } from "react"
|
import { use, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
import { trpc } from "@scandic-hotels/trpc/client"
|
import { trpc } from "@scandic-hotels/trpc/client"
|
||||||
|
|
||||||
import { createMyStayStore } from "@/stores/my-stay"
|
import { createMyStayStore } from "@/stores/my-stay"
|
||||||
@@ -91,9 +92,15 @@ export default function MyStayProvider({
|
|||||||
|
|
||||||
const rooms = [data.booking, ...linkedReservations]
|
const rooms = [data.booking, ...linkedReservations]
|
||||||
|
|
||||||
|
// Following the API logic for determining if booking is in the past.
|
||||||
|
// This is the same logic as used for separating stays in /future and /past endpoints on the API
|
||||||
|
const now = dt()
|
||||||
|
const isPastBooking = dt(data.booking.checkInDate).isBefore(now, "day")
|
||||||
|
|
||||||
const hasInvalidatedQueryAndRefetched =
|
const hasInvalidatedQueryAndRefetched =
|
||||||
(isFetchedAfterMount && data) ||
|
(isFetchedAfterMount && data) ||
|
||||||
(linkedReservationsIsFetchedAfterMount && linkedReservations)
|
(linkedReservationsIsFetchedAfterMount && linkedReservations)
|
||||||
|
|
||||||
if (!storeRef.current || hasInvalidatedQueryAndRefetched) {
|
if (!storeRef.current || hasInvalidatedQueryAndRefetched) {
|
||||||
storeRef.current = createMyStayStore({
|
storeRef.current = createMyStayStore({
|
||||||
breakfastPackages,
|
breakfastPackages,
|
||||||
@@ -104,6 +111,7 @@ export default function MyStayProvider({
|
|||||||
rooms,
|
rooms,
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
isPastBooking,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function createMyStayStore({
|
|||||||
rooms,
|
rooms,
|
||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
|
isPastBooking,
|
||||||
}: InitialState) {
|
}: InitialState) {
|
||||||
const rates = {
|
const rates = {
|
||||||
change: intl.formatMessage({
|
change: intl.formatMessage({
|
||||||
@@ -79,6 +80,7 @@ export function createMyStayStore({
|
|||||||
savedCreditCards,
|
savedCreditCards,
|
||||||
totalPoints,
|
totalPoints,
|
||||||
totalPrice,
|
totalPrice,
|
||||||
|
isPastBooking,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
closeManageStay() {
|
closeManageStay() {
|
||||||
|
|||||||
@@ -57,12 +57,18 @@ export interface MyStayState {
|
|||||||
savedCreditCards: CreditCard[] | null
|
savedCreditCards: CreditCard[] | null
|
||||||
totalPoints: number
|
totalPoints: number
|
||||||
totalPrice: string
|
totalPrice: string
|
||||||
|
isPastBooking: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialState
|
export interface InitialState
|
||||||
extends Pick<
|
extends Pick<
|
||||||
MyStayState,
|
MyStayState,
|
||||||
"breakfastPackages" | "hotel" | "refId" | "savedCreditCards" | "isLoggedIn"
|
| "breakfastPackages"
|
||||||
|
| "hotel"
|
||||||
|
| "refId"
|
||||||
|
| "savedCreditCards"
|
||||||
|
| "isLoggedIn"
|
||||||
|
| "isPastBooking"
|
||||||
> {
|
> {
|
||||||
intl: IntlShape
|
intl: IntlShape
|
||||||
roomCategories: RoomCategories
|
roomCategories: RoomCategories
|
||||||
|
|||||||
Reference in New Issue
Block a user