feat: refactor of my stay

This commit is contained in:
Simon Emanuelsson
2025-04-25 14:08:14 +02:00
committed by Simon.Emanuelsson
parent b5deb84b33
commit ec087a3d15
208 changed files with 5458 additions and 4569 deletions

View File

@@ -0,0 +1,19 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
import Button from "@/components/TempDesignSystem/Button"
export default function CustomerSupport() {
const intl = useIntl()
return (
<DialogTrigger>
<Button fullWidth intent="secondary" size="small">
{intl.formatMessage({ defaultMessage: "Customer Support" })}
</Button>
<CustomerSupportModal />
</DialogTrigger>
)
}

View File

@@ -0,0 +1,18 @@
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);
}
}

View File

@@ -0,0 +1,21 @@
"use client"
import { useIntl } from "react-intl"
import Link from "@/components/TempDesignSystem/Link"
import CustomerSupport from "./CustomerSupport"
import styles from "./cancelled.module.css"
export default function Cancelled() {
const intl = useIntl()
return (
<>
{/* (S) TODO - Link to where?? */}
<Link className={styles.link} href="#">
{intl.formatMessage({ defaultMessage: "Rebook" })}
</Link>
<CustomerSupport />
</>
)
}

View File

@@ -0,0 +1,30 @@
.links {
display: grid;
gap: var(--Space-x05);
}
.link {
align-items: center;
background: var(--Surface-Feedback-Information);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: var(--Corner-radius-Medium);
color: var(--Text-Interactive-Default);
display: flex;
flex-direction: column;
gap: var(--Space-x1);
padding: var(--Space-x3);
/* text-decoration: none; */
text-decoration-line: underline;
text-decoration-style: solid;
text-decoration-skip-ink: none;
text-decoration-thickness: auto;
text-underline-offset: auto;
text-underline-position: from-font;
}
@media screen and (min-width: 768px) {
.links {
gap: var(--Space-x3);
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -0,0 +1,78 @@
"use client"
import Link from "next/link"
import { Dialog } from "react-aria-components"
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 Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import styles from "./customerSupport.module.css"
export default function CustomerSupportModal() {
const intl = useIntl()
const { email, phone } = useMyStayStore((state) => ({
email: state.hotel.contactInformation.email,
phone: state.hotel.contactInformation.phoneNumber,
}))
const title = intl.formatMessage({ defaultMessage: "Customer service" })
const contact = intl.formatMessage(
{
defaultMessage:
"Please call {phone} or email us at {email} for assistance with your order.",
},
{ email, phone }
)
return (
<Modal>
<Dialog>
{({ close }) => (
<Modal.Content>
<Modal.Content.Header handleClose={close} title={title}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{contact}</p>
</Typography>
</Modal.Content.Header>
<Modal.Content.Body>
<div className={styles.links}>
<Link className={styles.link} href={`tel:${phone}`}>
<MaterialIcon color="Icon/Interactive/Default" icon="call" />
<Typography variant="Title/Subtitle/md">
<span>
{intl.formatMessage({
defaultMessage: "Make a call",
})}
</span>
</Typography>
</Link>
<Link className={styles.link} href={`mailto:${email}`}>
<MaterialIcon color="Icon/Interactive/Default" icon="mail" />
<Typography variant="Title/Subtitle/md">
<span>
{intl.formatMessage({
defaultMessage: "Send an email",
})}
</span>
</Typography>
</Link>
</div>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={close}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary intent="secondary" onClick={close}>
{intl.formatMessage({ defaultMessage: "Close" })}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)}
</Dialog>
</Modal>
)
}

View File

@@ -0,0 +1,43 @@
"use client"
import { Button as ButtonRAC } from "react-aria-components"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackMyStayPageLink } from "@/utils/tracking"
import styles from "./button.module.css"
export default function AddToCalendarButton({
disabled,
onPress,
}: {
disabled?: boolean
onPress: () => void
}) {
const intl = useIntl()
function handleAddToCalendar() {
trackMyStayPageLink("add to calendar")
onPress()
}
return (
<ButtonRAC
className={styles.button}
isDisabled={disabled}
onPress={handleAddToCalendar}
>
<MaterialIcon color="Icon/Interactive/Default" icon="calendar_add_on" />
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.text}>
{intl.formatMessage({
defaultMessage: "Add to calendar",
})}
</span>
</Typography>
</ButtonRAC>
)
}

View File

@@ -0,0 +1,18 @@
.button {
align-items: center;
background: none;
border: none;
cursor: pointer;
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x1) 0;
width: 100%;
&:disabled {
color: var(--Scandic-Grey-40);
}
}
.text {
color: var(--Text-Interactive-Default);
}

View File

@@ -0,0 +1,57 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
import { generateDateTime } from "@/components/HotelReservation/BookingConfirmation/Header/Actions/helpers"
import { dateHasPassed } from "../utils"
import AddToCalendarButton from "./AddToCalendarButton"
import type { EventAttributes } from "ics"
export default function AddToCalendarAction() {
const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore(
(state) => ({
checkInDate: state.bookedRoom.checkInDate,
checkOutDate: state.bookedRoom.checkOutDate,
createDateTime: state.bookedRoom.createDateTime,
hotel: state.hotel,
})
)
const calendarEvent: EventAttributes = {
busyStatus: "FREE",
categories: ["booking", "hotel", "stay"],
created: generateDateTime(createDateTime),
description: hotel.hotelContent.texts.descriptions?.medium,
end: generateDateTime(checkOutDate),
endInputType: "utc",
geo: {
lat: hotel.location.latitude,
lon: hotel.location.longitude,
},
location: `${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city} ${hotel.address.country}`,
start: generateDateTime(checkInDate),
startInputType: "utc",
status: "CONFIRMED",
title: hotel.name,
url: hotel.contactInformation.websiteUrl,
}
const disabled = dateHasPassed(
checkInDate,
hotel.hotelFacts.checkin.checkInTime
)
return (
<AddToCalendar
checkInDate={checkInDate}
event={calendarEvent}
hotelName={hotel.name}
renderButton={(onPress) => (
<AddToCalendarButton disabled={disabled} onPress={onPress} />
)}
/>
)
}

View File

@@ -0,0 +1,46 @@
"use client"
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"
interface AlertsProps extends React.PropsWithChildren {
closeModal: () => void
}
export default function Alerts({ children, closeModal }: AlertsProps) {
const intl = useIntl()
const mainRoom = useMyStayStore((state) => state.bookedRoom)
if (!mainRoom) {
const title = intl.formatMessage({ defaultMessage: "Cancel stay" })
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title} />
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "Contact the person who booked the stay",
})}
text={intl.formatMessage({
defaultMessage:
"As this is a multiroom stay, the cancellation has to be done by the person who made the booking. Please call 08-517 517 00 to talk to our customer service if you would need further assistance.",
})}
/>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}
return <>{children}</>
}

View File

@@ -0,0 +1,78 @@
"use client"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
import { formatPrice } from "@/utils/numberFormatting"
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
export default function CancelStayPriceContainer() {
const intl = useIntl()
const { bookedRoom, nights, rooms } = useMyStayStore((state) => ({
bookedRoom: state.bookedRoom,
nights: dt(state.bookedRoom.checkOutDate)
.startOf("day")
.diff(dt(state.bookedRoom.checkInDate).startOf("day"), "days"),
rooms: state.rooms,
}))
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
if (!Array.isArray(formRooms)) {
return null
}
const { totalAdults, totalChildren } = formRooms.reduce(
(total, formRoom) => {
if (formRoom.checked) {
const room = rooms.find(
(r) => r.confirmationNumber === formRoom.confirmationNumber
)
if (room) {
total.totalAdults = total.totalAdults + room.adults
if (room.childrenInRoom.length) {
total.totalChildren =
total.totalChildren + room.childrenInRoom.length
}
}
}
return total
},
{ totalAdults: 0, totalChildren: 0 }
)
const adultsText = intl.formatMessage(
{
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: totalAdults }
)
const childrenText = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: totalChildren }
)
const nightsText = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: nights }
)
return (
<PriceContainer
adultsText={adultsText}
childrenText={childrenText}
nightsText={nightsText}
price={formatPrice(intl, 0, bookedRoom.currencyCode)}
text={intl.formatMessage({ defaultMessage: "Total due" })}
totalChildren={totalChildren}
/>
)
}

View File

@@ -0,0 +1,111 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import styles from "./multiroom.module.css"
import type { Room } from "@/types/stores/my-stay"
export default function Multiroom() {
const intl = useIntl()
const rooms = useMyStayStore((state) => state.rooms)
const notCancelableRooms = rooms.filter((r) => !r.isCancelable)
const cancelableRooms = rooms.filter((r) => !r.isCancelled && r.isCancelable)
const isSingleRoom = rooms.length === 1
if (isSingleRoom) {
return null
}
const myRooms = intl.formatMessage({ defaultMessage: "My rooms" })
const selectRoom = intl.formatMessage({
defaultMessage: "Select room",
})
const cannotBeCancelled = intl.formatMessage({
defaultMessage: "Cannot be cancelled",
})
if (notCancelableRooms.length) {
return (
<div className={styles.wrapper}>
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
defaultMessage: "This stay has multiple terms.",
})}
</p>
</Typography>
<div className={styles.container}>
<List rooms={cancelableRooms} title={selectRoom} />
<List disabled rooms={notCancelableRooms} title={cannotBeCancelled} />
</div>
</div>
)
}
return <List rooms={cancelableRooms} title={myRooms} />
}
interface ListProps {
disabled?: boolean
rooms: Room[]
title: string
}
function List({ disabled = false, rooms, title }: ListProps) {
const intl = useIntl()
const refMsg = intl.formatMessage({ defaultMessage: "Ref" })
return (
<div className={styles.rooms}>
<Typography variant="Title/Overline/sm">
<p>{title}</p>
</Typography>
<ul className={styles.list}>
{rooms.map((room) => {
const roomNumber = room.roomNumber
return (
<li key={room.confirmationNumber}>
<Checkbox
className={styles.checkbox}
name={`rooms.${roomNumber - 1}.checked`}
registerOptions={{ disabled }}
>
<div className={styles.room}>
<div className={styles.chip}>
<Typography variant="Tag/sm">
<p className={styles.chipText}>
{intl.formatMessage(
{
defaultMessage: "Room {roomIndex}",
},
{
roomIndex: roomNumber,
}
)}
</p>
</Typography>
</div>
<Typography variant="Title/Subtitle/md">
<p>{room.roomName}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable formatjs/no-literal-string-in-jsx */}
<p>
<strong>{refMsg}:</strong> {room.confirmationNumber}
</p>
</Typography>
</div>
</Checkbox>
</li>
)
})}
</ul>
</div>
)
}

View File

@@ -0,0 +1,74 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--Space-x3);
}
.container {
display: flex;
flex-direction: column;
gap: var(--Space-x5);
}
.rooms {
display: flex;
flex-direction: column;
gap: var(--Space-x1);
}
.list {
display: flex;
flex-direction: column;
gap: var(--Space-x1);
list-style: none;
margin: 0;
padding: var(--Space-x05) 0 0;
}
.checkbox {
background: var(--Background-Primary);
border: 2px solid transparent;
border-radius: var(--Corner-radius-md);
padding: var(--Space-x2) var(--Space-x15);
}
.checkbox:has(input:checked) {
border-color: var(--Border-Interactive-Selected);
}
.checkbox:has(input:checked) span[class*="checkbox_checkbox_"] {
background-color: var(--Surface-UI-Fill-Active);
}
.checkbox:has(input:disabled) {
background-color: var(--Surface-UI-Fill-Disabled);
border: 1px solid var(--Border-Interactive-Disabled);
cursor: not-allowed;
}
.checkbox:has(input:disabled) .chip {
background-color: var(--Surface-UI-Fill-Disabled);
border: 1px solid var(--Text-Interactive-Disabled);
}
.checkbox:has(input:disabled) p {
color: var(--Text-Interactive-Disabled);
}
.room {
align-items: center;
display: grid;
gap: var(--Space-x1);
grid-template-columns: auto 1fr auto;
width: 100%;
}
.chip {
background-color: var(--Surface-Brand-Accent-Default);
border-radius: var(--Corner-radius-sm);
padding: var(--Space-x1);
}
.chipText {
color: var(--Text-Heading);
}

View File

@@ -0,0 +1,9 @@
.form {
display: flex;
flex-direction: column;
gap: var(--Space-x5);
}
.textDefault {
color: var(--Text-Default);
}

View File

@@ -0,0 +1,127 @@
"use client"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import useLang from "@/hooks/useLang"
import CancelStayPriceContainer from "../CancelStayPriceContainer"
import Multiroom from "./Multiroom"
import styles from "./confirmation.module.css"
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
interface CancelStayConfirmationProps {
closeModal: () => void
onSubmit: (data: CancelStayFormValues) => void
}
export default function CancelStayConfirmation({
closeModal,
onSubmit,
}: CancelStayConfirmationProps) {
const intl = useIntl()
const lang = useLang()
const { handleSubmit } = useFormContext<CancelStayFormValues>()
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
const { fromDate, hotel, isCancelable, rate, toDate } = useMyStayStore(
(state) => ({
fromDate: state.bookedRoom.checkInDate,
hotel: state.hotel,
isCancelable: state.bookedRoom.isCancelable,
rate: state.bookedRoom.rate,
toDate: state.bookedRoom.checkOutDate,
})
)
const checkInDate = dt(fromDate).locale(lang).format("dddd D MMM YYYY")
const checkOutDate = dt(toDate).locale(lang).format("dddd D MMM YYYY")
const title = intl.formatMessage({ defaultMessage: "Cancel booking" })
const primaryLabel = intl.formatMessage({
defaultMessage: "Cancel stay",
})
const secondaryLabel = intl.formatMessage({
defaultMessage: "Back",
})
const notCancelableText = intl.formatMessage(
{
defaultMessage:
"Your stay has been booked with <strong>{rate}</strong> terms which unfortunately doesnt allow for cancellation.",
},
{
rate,
strong: (str) => <strong>{str}</strong>,
}
)
const text = intl.formatMessage(
{
defaultMessage:
"Are you sure you want to cancel your stay at <strong>{hotel}</strong> from <strong>{checkInDate}</strong> to <strong>{checkOutDate}?</strong> This can't be reversed.",
},
{
checkInDate,
checkOutDate,
hotel: hotel.name,
strong: (str) => <strong>{str}</strong>,
}
)
const isValid = Array.isArray(formRooms)
? formRooms.some((r) => r.checked)
: false
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title}>
<Typography>
<p className={styles.textDefault}>
{isCancelable ? text : notCancelableText}
</p>
</Typography>
</Modal.Content.Header>
<Modal.Content.Body>
<form
className={styles.form}
id="cancel-stay"
onSubmit={handleSubmit(onSubmit)}
>
{isCancelable ? (
<>
<Multiroom />
<CancelStayPriceContainer />
</>
) : null}
</form>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{secondaryLabel}
</Modal.Content.Footer.Secondary>
{isCancelable ? (
<Modal.Content.Footer.Primary
disabled={!isValid}
form="cancel-stay"
intent="secondary"
type="submit"
>
{primaryLabel}
</Modal.Content.Footer.Primary>
) : (
<Modal.Content.Footer.Primary intent="secondary" onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Close" })}
</Modal.Content.Footer.Primary>
)}
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,9 @@
.toastContainer {
display: flex;
flex-direction: column;
gap: var(--Space-x05);
}
.textDefault {
color: var(--Text-Default);
}

View File

@@ -0,0 +1,170 @@
"use client"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@/lib/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import CancelStayPriceContainer from "../CancelStayPriceContainer"
import styles from "./finalConfirmation.module.css"
import type { CancelStayFormValues } from "@/types/components/hotelReservation/myStay/cancelStay"
interface FinalConfirmationProps {
closeModal: () => void
}
export default function FinalConfirmation({
closeModal,
}: FinalConfirmationProps) {
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const formRooms = useWatch<CancelStayFormValues>({ name: "rooms" })
const { bookedRoom, rooms } = useMyStayStore((state) => ({
bookedRoom: state.bookedRoom,
rooms: state.rooms,
}))
const cancelledStayMsg = intl.formatMessage({
defaultMessage: "Your stay was cancelled",
})
const sorryMsg = intl.formatMessage({
defaultMessage: "Were sorry that things didnt work out.",
})
const cancelBookingsMutation = trpc.booking.cancelMany.useMutation({
onSuccess(data, variables) {
const allCancellationsWentThrough = data.every((cancelled) => cancelled)
if (allCancellationsWentThrough) {
if (data.length === rooms.length) {
toast.success(
<div className={styles.toastContainer}>
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.textDefault}>{cancelledStayMsg}</span>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.textDefault}>{sorryMsg}</span>
</Typography>
</div>
)
} else {
const cancelledRooms = rooms.filter((r) =>
variables.confirmationNumbers.includes(r.confirmationNumber)
)
for (const cancelledRoom of cancelledRooms) {
toast.success(
<div className={styles.toastContainer}>
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.textDefault}>
<strong>
{intl.formatMessage(
{ defaultMessage: "{roomName} room was cancelled" },
{ roomName: cancelledRoom.roomName }
)}
</strong>
</span>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.textDefault}>
{intl.formatMessage({
defaultMessage:
"Your Stay is still active with the other room",
})}
</span>
</Typography>
</div>
)
}
}
} else {
toast.warning(
intl.formatMessage({
defaultMessage:
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
})
)
}
utils.booking.get.invalidate({
confirmationNumber: bookedRoom.confirmationNumber,
})
utils.booking.linkedReservations.invalidate({
lang,
rooms: bookedRoom.linkedReservations,
})
closeModal()
},
onError() {
toast.error(
intl.formatMessage({
defaultMessage: "Something went wrong. Please try again later.",
})
)
},
})
function cancelBooking() {
if (Array.isArray(formRooms)) {
const confirmationNumbersToCancel = formRooms
.filter((r) => r.checked)
.map((r) => r.confirmationNumber)
if (confirmationNumbersToCancel.length) {
cancelBookingsMutation.mutate({
confirmationNumbers: confirmationNumbersToCancel,
language: lang,
})
}
} else {
toast.error(
intl.formatMessage({
defaultMessage: "Something went wrong. Please try again later.",
})
)
}
}
const confirm = intl.formatMessage({
defaultMessage: "Confirm cancellation",
})
const dontCancel = intl.formatMessage({
defaultMessage: "Don't cancel",
})
const text = intl.formatMessage({
defaultMessage: "Are you sure you want to continue with the cancellation?",
})
const title = intl.formatMessage({
defaultMessage: "Cancel booking",
})
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title}>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>{text}</p>
</Typography>
</Modal.Content.Header>
<Modal.Content.Body>
<CancelStayPriceContainer />
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{dontCancel}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary
disabled={cancelBookingsMutation.isPending}
onClick={cancelBooking}
>
{confirm}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,60 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useMyStayStore } from "@/stores/my-stay"
import CancelStayConfirmation from "./Confirmation"
import FinalConfirmation from "./FinalConfirmation"
import {
type CancelStayFormValues,
cancelStaySchema,
} from "@/types/components/hotelReservation/myStay/cancelStay"
interface StepsProps {
closeModal: () => void
}
export default function Steps({ closeModal }: StepsProps) {
const [confirm, setConfirm] = useState(false)
const rooms = useMyStayStore((state) => state.rooms)
const methods = useForm<CancelStayFormValues>({
mode: "onSubmit",
reValidateMode: "onChange",
resolver: zodResolver(cancelStaySchema),
values: {
rooms: rooms.map((room, idx) => ({
// Single room booking
checked: rooms.length === 1,
confirmationNumber: room.confirmationNumber,
id: idx + 1,
})),
},
})
function handleSubmit(data: CancelStayFormValues) {
const checkedRooms = data.rooms.filter((r) => r.checked)
if (checkedRooms.length) {
setConfirm(true)
}
}
const stepOne = !confirm
const stepTwo = confirm
return (
<FormProvider {...methods}>
{/* Step 1 */}
{stepOne ? (
<CancelStayConfirmation
closeModal={closeModal}
onSubmit={handleSubmit}
/>
) : null}
{/* Step 2 */}
{stepTwo ? <FinalConfirmation closeModal={closeModal} /> : null}
</FormProvider>
)
}

View File

@@ -0,0 +1,25 @@
.modalText {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.rooms {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}
.roomContainer {
display: flex;
padding: var(--Spacing-x2);
background-color: var(--Base-Background-Primary-Normal);
border-radius: var(--Corner-radius-Medium);
align-items: center;
gap: var(--Spacing-x1);
}
.roomInfo {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,28 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Alerts from "./Alerts"
import Steps from "./Steps"
export default function CancelStay() {
const intl = useIntl()
return (
<DialogTrigger>
<Modal.Button icon="cancel">
{intl.formatMessage({ defaultMessage: "Cancel stay" })}
</Modal.Button>
<Modal>
<Dialog>
{({ close }) => (
<Alerts closeModal={close}>
<Steps closeModal={close} />
</Alerts>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,42 @@
"use client"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function CannotChangeDate({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "Contact customer service",
})}
text={intl.formatMessage({
defaultMessage:
"Please contact customer service to update the dates.",
})}
/>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,42 @@
"use client"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function MultiRoomBooking({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "Contact customer service",
})}
text={intl.formatMessage({
defaultMessage:
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please contact customer service to update the dates.",
})}
/>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,42 @@
"use client"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function NotMainRoom({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "Contact the person who booked the stay",
})}
text={intl.formatMessage({
defaultMessage:
"As this is a multiroom stay, any dates changes are applicable to all rooms. Please ask the person who booked the stay to contact customer service.",
})}
/>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,31 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import CannotChangeDate from "./CannotChangeDate"
import MultiRoomBooking from "./MultiRoomBooking"
import NotMainRoom from "./NotMainRoom"
export default function Alerts({
children,
closeModal,
}: React.PropsWithChildren<{ closeModal: () => void }>) {
const { canChangeDate, mainRoom, multiRoom } = useMyStayStore((state) => ({
canChangeDate: state.bookedRoom.canChangeDate,
mainRoom: state.bookedRoom.mainRoom,
multiRoom: state.bookedRoom.multiRoom,
}))
if (multiRoom) {
return <MultiRoomBooking closeModal={closeModal} />
}
if (!mainRoom) {
return <NotMainRoom closeModal={closeModal} />
}
if (!canChangeDate) {
return <CannotChangeDate closeModal={closeModal} />
}
return <>{children}</>
}

View File

@@ -0,0 +1,64 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./priceAndDate.module.css"
interface PriceAndDateProps {
checkInDate: string
checkOutDate: string
label: string
price: string
striked?: boolean
}
export default function PriceAndDate({
checkInDate,
checkOutDate,
label,
price,
striked = false,
}: PriceAndDateProps) {
const intl = useIntl()
const checkInMsg = intl.formatMessage({
defaultMessage: "Check-in",
})
const checkOutMsg = intl.formatMessage({
defaultMessage: "Check-out",
})
return (
<div className={styles.container}>
<div className={styles.item}>
<Typography variant="Body/Paragraph/mdBold">
<p className={styles.textDefault}>{label}</p>
</Typography>
<Typography variant="Body/Paragraph/mdBold">
<p className={styles.textSecondary}>{price}</p>
</Typography>
</div>
<div className={styles.item}>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textSecondary}>{checkInMsg}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>
{striked ? <s>{checkInDate}</s> : checkInDate}
</p>
</Typography>
</div>
<div className={styles.item}>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textSecondary}>{checkOutMsg}</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.textDefault}>
{striked ? <s>{checkOutDate}</s> : checkOutDate}
</p>
</Typography>
</div>
</div>
)
}

View File

@@ -0,0 +1,18 @@
.container {
display: flex;
flex-direction: column;
gap: var(--Space-x1);
}
.item {
display: flex;
justify-content: space-between;
}
.textDefault {
color: var(--Text-Default);
}
.textSecondary {
color: var(--Text-Secondary);
}

View File

@@ -0,0 +1,11 @@
.container {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.dateComparison {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}

View File

@@ -0,0 +1,183 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
import Divider from "@/components/TempDesignSystem/Divider"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import PriceAndDate from "./PriceAndDate"
import styles from "./confirmation.module.css"
import type { Lang } from "@/constants/languages"
interface ConfirmationProps {
checkInDate: string
checkOutDate: string
closeModal: () => void
newPrice: string
}
function formatDate(date: Date | string, lang: Lang) {
return dt(date).locale(lang).format("dddd, DD MMM, YYYY")
}
export default function Confirmation({
checkInDate,
checkOutDate,
closeModal,
newPrice,
}: ConfirmationProps) {
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const { bookedRoom, oldPrice, totalAdults, totalChildren } = useMyStayStore(
(state) => ({
bookedRoom: state.bookedRoom,
oldPrice: state.totalPrice,
totalAdults: state.rooms.reduce(
(total, room) => total + (room.isCancelled ? 0 : room.adults),
0
),
totalChildren: state.rooms.reduce(
(total, room) =>
total + (room.isCancelled ? 0 : room.childrenInRoom.length),
0
),
})
)
const updateBooking = trpc.booking.update.useMutation({
onSuccess: (updatedBooking) => {
if (updatedBooking) {
utils.booking.get.invalidate({
confirmationNumber: updatedBooking.confirmationNumber,
})
toast.success(
intl.formatMessage({
defaultMessage: "Your stay was updated",
})
)
closeModal()
} else {
toast.error(
intl.formatMessage({
defaultMessage: "Failed to update your stay",
})
)
}
},
onError: () => {
toast.error(
intl.formatMessage({
defaultMessage: "Failed to update your stay",
})
)
},
})
function handleModifyStay() {
updateBooking.mutate({
confirmationNumber: bookedRoom.confirmationNumber,
checkInDate,
checkOutDate,
})
}
const originalCheckIn = formatDate(bookedRoom.checkInDate, lang)
const originalCheckOut = formatDate(bookedRoom.checkOutDate, lang)
const newCheckIn = formatDate(checkInDate, lang)
const newCheckOut = formatDate(checkOutDate, lang)
const nights = dt(newCheckOut)
.startOf("day")
.diff(dt(newCheckIn).startOf("day"), "days")
const nightsText = intl.formatMessage(
{
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: nights }
)
const newDatesLabel = intl.formatMessage({
defaultMessage: "New dates",
})
const oldDatesLabel = intl.formatMessage({
defaultMessage: "Old dates",
})
const title = intl.formatMessage({
defaultMessage: "Confirm date change",
})
const totalDueMsg = intl.formatMessage({
defaultMessage: "Total due",
})
const adultsText = intl.formatMessage(
{
defaultMessage: "{totalAdults, plural, one {# adult} other {# adults}}",
},
{ totalAdults: totalAdults }
)
const childrenText = intl.formatMessage(
{
defaultMessage:
"{totalChildren, plural, one {# child} other {# children}}",
},
{ totalChildren: totalChildren }
)
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title} />
<Modal.Content.Body>
<div className={styles.container}>
<div className={styles.dateComparison}>
<PriceAndDate
checkInDate={originalCheckIn}
checkOutDate={originalCheckOut}
label={oldDatesLabel}
price={oldPrice}
striked
/>
<Divider color="primaryLightSubtle" />
<PriceAndDate
checkInDate={newCheckIn}
checkOutDate={newCheckOut}
label={newDatesLabel}
price={newPrice}
/>
</div>
<PriceContainer
adultsText={adultsText}
childrenText={childrenText}
nightsText={nightsText}
price={newPrice}
text={totalDueMsg}
totalChildren={totalChildren}
/>
</div>
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary
disabled={updateBooking.isPending}
onClick={handleModifyStay}
>
{intl.formatMessage({ defaultMessage: "Confirm" })}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,21 @@
"use client"
import { useIntl } from "react-intl"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function Error() {
const intl = useIntl()
return (
<Alert
type={AlertTypeEnum.Alarm}
heading={intl.formatMessage({
defaultMessage: "Error",
})}
text={intl.formatMessage({
defaultMessage: "Something went wrong!",
})}
/>
)
}

View File

@@ -0,0 +1,21 @@
"use client"
import { useIntl } from "react-intl"
import Alert from "@/components/TempDesignSystem/Alert"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function NoAvailability() {
const intl = useIntl()
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
defaultMessage: "No availability",
})}
text={intl.formatMessage({
defaultMessage: "No single rooms are available on these dates",
})}
/>
)
}

View File

@@ -0,0 +1,14 @@
.button {
background-color: var(--Main-Grey-White);
border-color: var(--Scandic-Beige-40);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Medium);
display: flex;
align-items: center;
justify-content: space-between;
min-width: 0; /* allow shrinkage */
height: 60px;
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
}

View File

@@ -0,0 +1,24 @@
"use client"
import { Button as ButtonRAC } from "react-aria-components"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./calendarButton.module.css"
interface CalendarButtonProps {
text: string
onClick: () => void
}
export default function CalendarButton({ text, onClick }: CalendarButtonProps) {
return (
<ButtonRAC onPress={onClick} className={styles.button}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{text}</span>
</Typography>
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
)
}

View File

@@ -0,0 +1,187 @@
"use client"
import { useState } from "react"
import { createPortal } from "react-dom"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
import Modal from "@/components/Modal"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import CalendarButton from "./CalendarButton"
import styles from "./newDates.module.css"
import type { DateRange } from "react-day-picker"
export default function NewDates() {
const { checkInDate, checkOutDate } = useMyStayStore((state) => ({
checkInDate: state.mainRoom.checkInDate,
checkOutDate: state.mainRoom.checkOutDate,
}))
const [showCheckInDatePicker, setShowCheckInDatePicker] = useState(false)
const [showCheckOutDatePicker, setShowCheckOutDatePicker] = useState(false)
const [selectedDates, setSelectedDates] = useState<DateRange>(() => ({
from: dt(checkInDate).startOf("day").toDate(),
to: dt(checkOutDate).startOf("day").toDate(),
}))
const intl = useIntl()
const lang = useLang()
const { setValue } = useFormContext()
// Calculate default number of days between check-in and check-out
const defaultDaysBetween = dt(checkOutDate)
.startOf("day")
.diff(dt(checkInDate).startOf("day"), "days")
function showCheckInPicker() {
// Update selected dates before showing picker
setSelectedDates((prev) => ({
from: prev.from ?? dt(checkInDate).startOf("day").toDate(),
to: prev.to ?? dt(checkOutDate).startOf("day").toDate(),
}))
setShowCheckInDatePicker(true)
setShowCheckOutDatePicker(false)
}
function showCheckOutPicker() {
// Update selected dates before showing picker
setSelectedDates((prev) => ({
from: prev.from ?? dt(checkInDate).startOf("day").toDate(),
to: prev.to ?? dt(checkOutDate).startOf("day").toDate(),
}))
setShowCheckOutDatePicker(true)
setShowCheckInDatePicker(false)
}
function handleCheckInDateSelect(date: Date) {
const newCheckIn = dt(date).startOf("day")
const currentCheckOut = dt(selectedDates.to).startOf("day")
// Calculate new check-out date based on defaultDaysBetween, only if new check-in is after current check-out
const newCheckOut = newCheckIn.isSameOrAfter(currentCheckOut)
? newCheckIn.add(defaultDaysBetween, "days")
: currentCheckOut
// Update selected dates state first
const newDates = {
from: newCheckIn.toDate(),
to: newCheckOut.toDate(),
}
setSelectedDates(newDates)
// Then update form values
setValue("checkInDate", newCheckIn.format("YYYY-MM-DD"))
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
}
function handleCheckOutDateSelect(date: Date) {
const newCheckOut = dt(date).startOf("day")
const currentCheckIn = dt(selectedDates.from).startOf("day")
// Only adjust check-in if new check-out is before current check-in
const newCheckIn = newCheckOut.isBefore(currentCheckIn)
? newCheckOut.subtract(defaultDaysBetween, "days")
: currentCheckIn
// Update selected dates state
const newDates = {
from: newCheckIn.toDate(),
to: newCheckOut.toDate(),
}
setSelectedDates(newDates)
// Then update form values
setValue("checkInDate", newCheckIn.format("YYYY-MM-DD"))
setValue("checkOutDate", newCheckOut.format("YYYY-MM-DD"))
}
const fromDate = selectedDates.from ?? dt(checkInDate).toDate()
const toDate = selectedDates.to ?? dt(checkOutDate).toDate()
return (
<>
<div className={styles.container}>
<div className={styles.checkInDate}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({
defaultMessage: "Check-in",
})}
</Caption>
<CalendarButton
text={dt(selectedDates.from ?? new Date())
.locale(lang)
.format("dddd, DD MMM, YYYY")}
onClick={showCheckInPicker}
/>
</div>
<div className={styles.checkOutDate}>
<Caption color="uiTextHighContrast" type="bold">
{intl.formatMessage({
defaultMessage: "Check-out",
})}
</Caption>
<CalendarButton
text={dt(selectedDates.to ?? new Date())
.locale(lang)
.format("dddd, DD MMM, YYYY")}
onClick={showCheckOutPicker}
/>
</div>
</div>
{showCheckInDatePicker &&
createPortal(
<Modal
isOpen={showCheckInDatePicker}
onToggle={() => setShowCheckInDatePicker(!showCheckInDatePicker)}
>
<DatePickerSingleDesktop
close={() => setShowCheckInDatePicker(false)}
handleOnSelect={handleCheckInDateSelect}
selectedDate={fromDate}
startMonth={fromDate}
/>
<DatePickerSingleMobile
close={() => setShowCheckInDatePicker(false)}
handleOnSelect={handleCheckInDateSelect}
selectedDate={fromDate}
hideHeader
/>
</Modal>,
document.body
)}
{showCheckOutDatePicker &&
createPortal(
<Modal
isOpen={showCheckOutDatePicker}
onToggle={() => setShowCheckOutDatePicker(!showCheckOutDatePicker)}
>
<DatePickerSingleDesktop
close={() => setShowCheckOutDatePicker(false)}
handleOnSelect={handleCheckOutDateSelect}
selectedDate={toDate}
startMonth={toDate}
/>
<DatePickerSingleMobile
close={() => setShowCheckOutDatePicker(false)}
handleOnSelect={handleCheckOutDateSelect}
selectedDate={toDate}
hideHeader
/>
</Modal>,
document.body
)}
</>
)
}

View File

@@ -0,0 +1,15 @@
.container {
background-color: var(--Base-Background-Primary-Normal);
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3);
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
.checkInDate,
.checkOutDate {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}

View File

@@ -0,0 +1,85 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import { toast } from "@/components/TempDesignSystem/Toasts"
import NoAvailability from "./Alerts/NoAvailability"
import NewDates from "./NewDates"
import {
type ChangeDatesFormProps,
type ChangeDatesSchema,
changeDatesSchema,
} from "@/types/components/hotelReservation/myStay/changeDates"
export default function Form({
checkAvailability,
closeModal,
noAvailability,
}: ChangeDatesFormProps) {
const intl = useIntl()
const { checkInDate, checkOutDate } = useMyStayStore((state) => ({
checkInDate: state.bookedRoom.checkInDate,
checkOutDate: state.bookedRoom.checkOutDate,
}))
const methods = useForm<ChangeDatesSchema>({
defaultValues: {
checkInDate: dt(checkInDate).format("YYYY-MM-DD"),
checkOutDate: dt(checkOutDate).format("YYYY-MM-DD"),
},
resolver: zodResolver(changeDatesSchema),
})
async function handleSubmit(values: ChangeDatesSchema) {
if (values.checkInDate && values.checkOutDate) {
await checkAvailability(values.checkInDate, values.checkOutDate)
} else {
toast.error(
intl.formatMessage({
defaultMessage: "Please select dates",
})
)
}
}
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(handleSubmit)}>
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
{noAvailability && <NoAvailability />}
<NewDates />
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary
disabled={methods.formState.isSubmitting}
intent="secondary"
type="submit"
>
{intl.formatMessage({
defaultMessage: "Check availability",
})}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
</form>
</FormProvider>
)
}

View File

@@ -0,0 +1,136 @@
"use client"
import { useSession } from "next-auth/react"
import { useState } from "react"
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import { sumPackages } from "@/components/HotelReservation/utils"
import useLang from "@/hooks/useLang"
import { isValidClientSession } from "@/utils/clientSession"
import { formatPrice } from "@/utils/numberFormatting"
import Confirmation from "./Confirmation"
import Form from "./Form"
import type { ChangeDatesStepsProps } from "@/types/components/hotelReservation/myStay/changeDates"
import { CurrencyEnum } from "@/types/enums/currency"
interface Dates {
fromDate: string
toDate: string
}
export default function Steps({ closeModal }: ChangeDatesStepsProps) {
const { data: session } = useSession()
const isLoggedIn = isValidClientSession(session)
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const [dates, setDates] = useState<Dates | null>(null)
const [newPrice, setNewPrice] = useState<string | null>(null)
const [noAvailability, setNoAvailability] = useState(false)
const { breakfast, currencyCode, hotelId, packages, room } = useMyStayStore(
(state) => ({
breakfast: state.bookedRoom.breakfast,
currencyCode: state.bookedRoom.currencyCode,
hotelId: state.bookedRoom.hotelId,
packages: state.bookedRoom.packages ?? [],
room: {
adults: state.bookedRoom.adults,
bookingCode: state.bookedRoom.bookingCode ?? undefined,
childrenInRoom: state.bookedRoom.childrenInRoom,
rateCode: state.bookedRoom.rateDefinition.rateCode,
roomTypeCode: state.bookedRoom.roomTypeCode,
},
})
)
async function checkAvailability(fromDate: string, toDate: string) {
setNoAvailability(false)
const data = await utils.hotel.availability.myStay.fetch({
booking: { fromDate, hotelId, room, toDate },
lang,
})
if (!data || !data.selectedRoom || !data.selectedRoom.roomsLeft) {
setNoAvailability(true)
return
}
setDates({ fromDate, toDate })
const pkgsSum = sumPackages(packages)
const extraPrice = pkgsSum.price + (breakfast?.localPrice.totalPrice || 0)
if (isLoggedIn && "member" in data.product && data.product.member) {
const { currency, pricePerStay } = data.product.member.localPrice
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
} else if ("public" in data.product && data.product.public) {
const { currency, pricePerStay } = data.product.public.localPrice
setNewPrice(formatPrice(intl, pricePerStay + extraPrice, currency))
} else if (
"corporateCheque" in data.product &&
data.product.corporateCheque.localPrice.additionalPricePerStay
) {
const { additionalPricePerStay, currency, numberOfCheques } =
data.product.corporateCheque.localPrice
setNewPrice(
formatPrice(
intl,
numberOfCheques,
CurrencyEnum.CC,
additionalPricePerStay + extraPrice,
currency?.toString() ?? pkgsSum.currency ?? currencyCode
)
)
} else if (
"redemption" in data.product &&
data.product.redemption.localPrice.additionalPricePerStay
) {
const { additionalPricePerStay, currency, pointsPerStay } =
data.product.redemption.localPrice
setNewPrice(
formatPrice(
intl,
pointsPerStay,
CurrencyEnum.POINTS,
additionalPricePerStay + extraPrice,
currency?.toString() ?? pkgsSum.currency ?? currencyCode
)
)
}
}
function goBackToSelectDates() {
setNewPrice(null)
setDates(null)
setNoAvailability(false)
}
const hasNewDate = newPrice && dates
const stepOne = !hasNewDate
const stepTwo = hasNewDate
return (
<>
{stepOne ? (
<Form
checkAvailability={checkAvailability}
closeModal={closeModal}
noAvailability={noAvailability}
/>
) : null}
{stepTwo ? (
<Confirmation
checkInDate={dates.fromDate}
checkOutDate={dates.toDate}
closeModal={goBackToSelectDates}
newPrice={newPrice}
/>
) : null}
</>
)
}

View File

@@ -0,0 +1,49 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import { dateHasPassed } from "../utils"
import Alerts from "./Alerts"
import Steps from "./Steps"
export default function ChangeDates() {
const intl = useIntl()
const { canChangeDate, checkInDate, checkInTime, isCancelled, priceType } =
useMyStayStore((state) => ({
canChangeDate: state.bookedRoom.canChangeDate,
checkInDate: state.bookedRoom.checkInDate,
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
isCancelled: state.bookedRoom.isCancelled,
priceType: state.bookedRoom.priceType,
}))
const isRewardNight = priceType === "points"
const isDisabled =
canChangeDate &&
!isCancelled &&
!isRewardNight &&
dateHasPassed(checkInDate, checkInTime)
const text = intl.formatMessage({ defaultMessage: "Change dates" })
return (
<DialogTrigger>
<Modal.Button icon="edit_calendar" isDisabled={isDisabled}>
{text}
</Modal.Button>
<Modal>
<Dialog>
{({ close }) => (
<Alerts closeModal={close}>
<Steps closeModal={close} />
</Alerts>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,18 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
export default function CustomerSupport() {
const intl = useIntl()
return (
<DialogTrigger>
<Modal.Button icon="support_agent">
{intl.formatMessage({ defaultMessage: "Customer support" })}
</Modal.Button>
<CustomerSupportModal />
</DialogTrigger>
)
}

View File

@@ -0,0 +1,48 @@
.loading {
display: flex;
align-items: center;
justify-content: center;
width: 640px;
max-width: 100%;
height: 640px;
max-height: 100%;
}
.form {
display: grid;
gap: var(--Spacing-x3);
}
.termsAndConditions {
color: var(--Text-Secondary);
display: grid;
gap: var(--Spacing-x2);
}
.termsAndConditions .checkbox span {
align-items: flex-start;
}
.guaranteeCost {
align-items: center;
background-color: var(--Base-Surface-Subtle-Normal);
border-radius: var(--Corner-radius-Medium);
display: flex;
gap: var(--Spacing-x3);
justify-content: flex-end;
padding: var(--Spacing-x2);
}
.guaranteeCostText {
align-items: flex-end;
display: flex;
flex-direction: column;
}
.baseTextHighContrast {
color: var(--Base-Text-High-contrast);
}
.textDefault {
color: var(--Text-Default);
}

View File

@@ -0,0 +1,194 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { PaymentMethodEnum } from "@/constants/booking"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
import { env } from "@/env/client"
import { useMyStayStore } from "@/stores/my-stay"
import PaymentOptionsGroup from "@/components/HotelReservation/EnterDetails/Payment/PaymentOptionsGroup"
import MySavedCards from "@/components/HotelReservation/MySavedCards"
import PaymentOption from "@/components/HotelReservation/PaymentOption"
import LoadingSpinner from "@/components/LoadingSpinner"
import Divider from "@/components/TempDesignSystem/Divider"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import { toast } from "@/components/TempDesignSystem/Toasts"
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay"
import { type GuaranteeFormData, paymentSchema } from "./schema"
import styles from "./form.module.css"
export default function Form() {
const intl = useIntl()
const lang = useLang()
const { confirmationNumber, currencyCode, hotelId, refId, savedCreditCards } =
useMyStayStore((state) => ({
confirmationNumber: state.bookedRoom.confirmationNumber,
currencyCode: state.bookedRoom.currencyCode,
hotelId: state.bookedRoom.hotelId,
refId: state.refId,
savedCreditCards: state.savedCreditCards,
}))
const methods = useForm<GuaranteeFormData>({
defaultValues: {
paymentMethod: savedCreditCards?.length
? savedCreditCards[0].id
: PaymentMethodEnum.card,
termsAndConditions: false,
},
mode: "all",
reValidateMode: "onChange",
resolver: zodResolver(paymentSchema),
})
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
const { guaranteeBooking, isLoading, handleGuaranteeError } =
useGuaranteeBooking(confirmationNumber)
if (isLoading) {
return (
<div className={styles.loading}>
<LoadingSpinner />
</div>
)
}
function handleGuaranteeLateArrival(data: GuaranteeFormData) {
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
trackGlaSaveCardAttempt(hotelId, savedCreditCard, "yes")
if (confirmationNumber) {
const card = savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined
guaranteeBooking.mutate({
confirmationNumber,
language: lang,
...(card && { card }),
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`,
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`,
})
} else {
handleGuaranteeError("No confirmation number")
toast.error(
intl.formatMessage({
defaultMessage: "Something went wrong!",
})
)
}
}
const guaranteeMsg = intl.formatMessage(
{
defaultMessage:
"By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.",
},
{
termsAndConditionsLink: (str) => (
<Link
variant="underscored"
color="peach80"
target="_blank"
href={bookingTermsAndConditions[lang]}
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
variant="underscored"
color="peach80"
target="_blank"
href={privacyPolicy[lang]}
>
{str}
</Link>
),
}
)
return (
<FormProvider {...methods}>
<form
className={styles.form}
id="guarantee"
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
>
{savedCreditCards?.length ? (
<MySavedCards savedCreditCards={savedCreditCards} />
) : null}
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl.formatMessage({
defaultMessage: "OTHER",
})
: undefined
}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
defaultMessage: "Credit card",
})}
/>
</PaymentOptionsGroup>
<div className={styles.termsAndConditions}>
<Checkbox className={styles.checkbox} name="termsAndConditions">
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>{guaranteeMsg}</p>
</Typography>
</Checkbox>
</div>
<div className={styles.guaranteeCost}>
<div className={styles.guaranteeCostText}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.textDefault}>
{intl.formatMessage({
defaultMessage: "Total due",
})}
</span>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.textDefault}>
{intl.formatMessage({
defaultMessage:
"Your card will only be charged in the event of a no-show",
})}
</span>
</Typography>
</div>
<Divider variant="vertical" color="subtle" />
<Typography variant="Title/Subtitle/md">
<span className={styles.baseTextHighContrast}>
{formatPrice(intl, 0, currencyCode)}
</span>
</Typography>
</div>
</form>
</FormProvider>
)
}

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
export const paymentSchema = z.object({
paymentMethod: z.string().nullable(),
termsAndConditions: z.boolean().refine((value) => value === true, {
message: "You must accept the terms and conditions",
}),
})
export interface GuaranteeFormData extends z.output<typeof paymentSchema> {}

View File

@@ -0,0 +1,69 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import { dateHasPassed } from "../utils"
import Form from "./Form"
export default function GuaranteeLateArrival() {
const intl = useIntl()
const { checkInDate, checkInTime, guaranteeInfo, isCancelled } =
useMyStayStore((state) => ({
checkInDate: state.bookedRoom.checkInDate,
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
guaranteeInfo: state.bookedRoom.guaranteeInfo,
isCancelled: state.bookedRoom.isCancelled,
}))
const guaranteeable =
!guaranteeInfo && !isCancelled && !dateHasPassed(checkInDate, checkInTime)
if (!guaranteeable) {
return null
}
const arriveLateMsg = intl.formatMessage({
defaultMessage:
"Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
})
const text = intl.formatMessage({
defaultMessage: "Guarantee late arrival",
})
return (
<DialogTrigger>
<Modal.Button icon="check">{text}</Modal.Button>
<Modal>
<Dialog>
{({ close }) => (
<Modal.Content>
<Modal.Content.Header handleClose={close} title={text}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{arriveLateMsg}</p>
</Typography>
</Modal.Content.Header>
<Modal.Content.Body>
<Form />
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={close}>
{intl.formatMessage({ defaultMessage: "Back" })}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary form="guarantee" type="submit">
{intl.formatMessage({ defaultMessage: "Guarantee" })}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,56 @@
"use client"
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { CancellationRuleEnum } from "@/constants/booking"
import { preliminaryReceipt } from "@/constants/routes/myStay"
import { useMyStayStore } from "@/stores/my-stay"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import { trackMyStayPageLink } from "@/utils/tracking"
import styles from "./view.module.css"
export default function ViewAndPrintReceipt() {
const intl = useIntl()
const lang = useLang()
const canDownloadInvoice = useMyStayStore(
(state) =>
!state.bookedRoom.isCancelled &&
!(
state.bookedRoom.rateDefinition.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM
)
)
if (!canDownloadInvoice) {
return null
}
function trackClick() {
trackMyStayPageLink("download invoice")
}
const printMsg = intl.formatMessage({
defaultMessage: "View and print receipt",
})
return (
<div onClickCapture={trackClick}>
<Link
className={styles.download}
href={preliminaryReceipt[lang]}
keepSearchParams
target="_blank"
>
<MaterialIcon color="Icon/Interactive/Default" icon="print" />
<Typography variant="Body/Paragraph/mdBold">
<span>{printMsg}</span>
</Typography>
</Link>
</div>
)
}

View File

@@ -0,0 +1,7 @@
.download {
align-items: center;
color: var(--Text-Interactive-Default);
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x1) 0;
}

View File

@@ -0,0 +1,5 @@
.list {
list-style: none;
margin: 0;
padding: 0;
}

View File

@@ -0,0 +1,21 @@
import AddToCalendar from "./AddToCalendar"
import CancelStay from "./CancelStay"
import ChangeDates from "./ChangeDates"
import CustomerSupport from "./CustomerSupport"
import GuaranteeLateArrival from "./GuaranteeLateArrival"
import ViewAndPrintReceipt from "./ViewAndPrintReceipt"
import styles from "./actions.module.css"
export default function Actions() {
return (
<div className={styles.list}>
<ChangeDates />
<GuaranteeLateArrival />
<AddToCalendar />
<ViewAndPrintReceipt />
<CustomerSupport />
<CancelStay />
</div>
)
}

View File

@@ -0,0 +1,7 @@
import { dt } from "@/lib/dt"
export function dateHasPassed(date: Date, time: string) {
const hour = dt(time, "HH:mm").hour()
const minute = dt(time, "HH:mm").minute()
return dt(date).hour(hour).minute(minute).isBefore(dt(), "minutes")
}

View File

@@ -0,0 +1,44 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import styles from "./info.module.css"
export default function Info() {
const intl = useIntl()
const text = intl.formatMessage({ defaultMessage: "Booking number" })
const { address, confirmationNumber, hotelName, phoneNumber } =
useMyStayStore((state) => ({
address: state.hotel.address,
confirmationNumber: state.bookedRoom.confirmationNumber,
hotelName: state.hotel.name,
phoneNumber: state.hotel.contactInformation.phoneNumber,
}))
return (
<div className={styles.container}>
<div className={styles.booking}>
<Typography variant="Tag/sm">
<span className={styles.text}>{text}</span>
</Typography>
<Typography variant="Title/sm">
<span className={styles.confirmationNumber}>
{confirmationNumber}
</span>
</Typography>
</div>
<Typography variant="Body/Supporting text (caption)/smRegular">
<div className={styles.address}>
<span>{hotelName}</span>
<span>{address.streetAddress}</span>
<span>{address.city}</span>
<span>{phoneNumber}</span>
</div>
</Typography>
</div>
)
}

View File

@@ -0,0 +1,29 @@
.container {
align-items: flex-start;
background-color: var(--Surface-Primary-OnSurface-Default);
border-radius: var(--Corner-radius-md);
display: flex;
flex-direction: column;
gap: var(--Space-x2);
justify-content: center;
padding: var(--Space-x15) var(--Space-x3);
}
.booking {
display: flex;
flex-direction: column;
gap: var(--Space-x05);
}
.text {
color: var(--Text-Default);
}
.confirmationNumber {
color: var(--Text-Heading);
}
.address {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,65 @@
"use client"
import {
Button as ButtonRAC,
Dialog,
DialogTrigger,
} from "react-aria-components"
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 Modal from "@/components/HotelReservation/MyStay/ReferenceCard/Modal"
import Actions from "./Actions"
import Info from "./Info"
import styles from "./manageStay.module.css"
export default function ManageStay() {
const intl = useIntl()
const allRoomsAreCancelled = useMyStayStore(
(state) => state.allRoomsAreCancelled
)
const color = allRoomsAreCancelled
? "Icon/Interactive/Disabled"
: "Icon/Inverted"
const manageStay = intl.formatMessage({
defaultMessage: "Manage stay",
})
return (
<DialogTrigger>
<ButtonRAC className={styles.trigger} isDisabled={allRoomsAreCancelled}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span>{manageStay}</span>
</Typography>
<MaterialIcon color={color} icon="keyboard_arrow_down" />
</ButtonRAC>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (
<>
<header className={styles.header}>
<Typography variant="Title/Subtitle/lg">
<span className={styles.title}>{manageStay}</span>
</Typography>
<ButtonRAC className={styles.close} onPress={close}>
<MaterialIcon color="Icon/Feedback/Neutral" icon="close" />
</ButtonRAC>
</header>
<div className={styles.content}>
<Actions />
<Info />
</div>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,52 @@
.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;
gap: var(--Space-x3);
}
.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-x3);
grid-template-columns: 1fr 1fr;
}

View File

@@ -0,0 +1,34 @@
"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 location = useMyStayStore((state) => state.hotel.location)
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${location.latitude},${location.longitude}`
return (
<>
<ManageStay />
<Link className={styles.link} href={directionsUrl} target="_blank">
<Typography variant="Body/Supporting text (caption)/smBold">
<span>
{intl.formatMessage({
defaultMessage: "Find us",
})}
</span>
</Typography>
<MaterialIcon color="Icon/Interactive/Default" icon="location_on" />
</Link>
</>
)
}

View File

@@ -0,0 +1,9 @@
.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;
justify-content: center;
text-decoration: none;
}

View File

@@ -0,0 +1,12 @@
.actionArea {
display: grid;
gap: var(--Spacing-x2);
}
@media (min-width: 768px) {
.actionArea {
gap: var(--Spacing-x2);
grid-template-columns: 1fr 1fr;
padding-top: var(--Spacing-x3);
}
}

View File

@@ -0,0 +1,16 @@
"use client"
import { useMyStayStore } from "@/stores/my-stay"
import Cancelled from "./Cancelled"
import NotCancelled from "./NotCancelled"
import styles from "./actions.module.css"
export default function Actions() {
const isCancelled = useMyStayStore((state) => state.bookedRoom.isCancelled)
return (
<div className={styles.actionArea}>
{isCancelled ? <Cancelled /> : <NotCancelled />}
</div>
)
}