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:
Christel Westerberg
2025-11-07 06:43:13 +00:00
parent 9a07dee05b
commit 6083eea5cc
57 changed files with 178 additions and 188 deletions

View File

@@ -0,0 +1,33 @@
"use client"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import styles from "./actionsButton.module.css"
import type { MaterialSymbolProps } from "@scandic-hotels/design-system/Icons/MaterialIcon/MaterialSymbol"
export default function ActionsButton({
icon,
text,
onPress,
isDisabled = false,
}: {
icon: MaterialSymbolProps["icon"]
text: string
onPress: () => void
isDisabled?: boolean
}) {
return (
<Button
variant="Text"
wrapping={false}
onPress={onPress}
isDisabled={isDisabled}
typography="Body/Paragraph/mdBold"
>
<MaterialIcon color="CurrentColor" icon={icon} className={styles.icon} />
<span>{text}</span>
</Button>
)
}

View File

@@ -0,0 +1,34 @@
"use client"
import { useIntl } from "react-intl"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
export default function AddToCalendarButton({
disabled,
onPress,
}: {
disabled?: boolean
onPress: () => void
}) {
const intl = useIntl()
function handleAddToCalendar() {
trackMyStayPageLink("add to calendar")
onPress()
}
return (
<ActionsButton
isDisabled={disabled}
icon="calendar_add_on"
text={intl.formatMessage({
id: "common.addToCalendar",
defaultMessage: "Add to calendar",
})}
onPress={handleAddToCalendar}
/>
)
}

View File

@@ -0,0 +1,66 @@
"use client"
import { usePathname } from "next/navigation"
import { AddToCalendar } from "@scandic-hotels/booking-flow/components/AddToCalendar"
import { generateDateTime } from "@scandic-hotels/booking-flow/components/BookingConfirmation/Header/Actions/helpers"
import { isWebview } from "@/constants/routes/webviews"
import { useMyStayStore } from "@/stores/my-stay"
import { dateHasPassed } from "../utils"
import AddToCalendarButton from "./AddToCalendarButton"
import type { EventAttributes } from "ics"
export default function AddToCalendarAction() {
const pathName = usePathname()
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
)
if (isWebview(pathName)) {
return null
}
return (
<AddToCalendar
checkInDate={checkInDate}
event={calendarEvent}
hotelName={hotel.name}
renderButton={(onPress) => (
<AddToCalendarButton disabled={disabled} onPress={onPress} />
)}
/>
)
}

View File

@@ -0,0 +1,54 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
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({
id: "booking.cancelStay",
defaultMessage: "Cancel stay",
})
return (
<Modal.Content>
<Modal.Content.Header handleClose={closeModal} title={title} />
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "myStay.actions.contactBooker",
defaultMessage: "Contact the person who booked the stay",
})}
text={intl.formatMessage({
id: "myStay.actions.contactBooker.multiroom.cancel",
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({
id: "common.back",
defaultMessage: "Back",
})}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}
return <>{children}</>
}

View File

@@ -0,0 +1,82 @@
"use client"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { useMyStayStore } from "@/stores/my-stay"
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
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.refId === formRoom.refId)
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(
{
id: "booking.numberOfAdults",
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{ adults: totalAdults }
)
const childrenText = intl.formatMessage(
{
id: "booking.numberOfChildren",
defaultMessage: "{children, plural, one {# child} other {# children}}",
},
{ children: totalChildren }
)
const nightsText = intl.formatMessage(
{
id: "booking.numberOfNights",
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({
id: "booking.totalDue",
defaultMessage: "Total due",
})}
totalChildren={totalChildren}
/>
)
}

View File

@@ -0,0 +1,120 @@
"use client"
import { useIntl } from "react-intl"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
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",
id: "myStay.cancelStay.myRooms",
})
const selectRoom = intl.formatMessage({
id: "booking.selectRoom",
defaultMessage: "Select room",
})
const cannotBeCancelled = intl.formatMessage({
id: "myStay.cancelStay.cannotBeCancelled",
defaultMessage: "Cannot be cancelled",
})
if (notCancelableRooms.length) {
return (
<div className={styles.wrapper}>
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
id: "myStay.cancelStay.multipleTermsNotice",
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({
id: "myStay.cancelStay.ref",
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(
{
id: "booking.roomIndex",
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-Active);
}
.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,142 @@
"use client"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { longDateWithYearFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/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(longDateWithYearFormat[lang])
const checkOutDate = dt(toDate)
.locale(lang)
.format(longDateWithYearFormat[lang])
const title = intl.formatMessage({
id: "common.cancelBooking",
defaultMessage: "Cancel booking",
})
const primaryLabel = intl.formatMessage({
id: "booking.cancelStay",
defaultMessage: "Cancel stay",
})
const secondaryLabel = intl.formatMessage({
id: "common.back",
defaultMessage: "Back",
})
const notCancelableText = intl.formatMessage(
{
id: "myStay.referenceCard.actions.cancelStay.notCancelableText",
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(
{
id: "myStay.referenceCard.actions.cancelStay.confirmationText",
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({
id: "common.close",
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,203 @@
"use client"
import { differenceInCalendarDays } from "date-fns"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trpc } from "@scandic-hotels/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import useLang from "@/hooks/useLang"
import { trackCancelStay } from "@/utils/tracking"
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({
id: "myStay.cancelStay.stayCancelledToastMessage",
defaultMessage: "Your stay was cancelled",
})
const sorryMsg = intl.formatMessage({
id: "myStay.cancelStay.sorryMessage",
defaultMessage: "Were sorry that things didnt work out.",
})
const cancelBookingsMutation = trpc.booking.cancel.useMutation({
onSuccess(data, variables) {
for (const confirmationNumber of data) {
if (confirmationNumber) {
const room = rooms.find(
(room) => room.confirmationNumber === confirmationNumber
)
const duration = differenceInCalendarDays(
bookedRoom.checkOutDate,
bookedRoom.checkInDate
)
const roomPrice = room?.roomPrice.perStay.local.price
trackCancelStay(
bookedRoom.hotelId,
duration,
confirmationNumber,
roomPrice
)
}
}
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.refIds.includes(r.refId)
)
for (const cancelledRoom of cancelledRooms) {
toast.success(
<div className={styles.toastContainer}>
<Typography variant="Body/Paragraph/mdBold">
<span className={styles.textDefault}>
<strong>
{intl.formatMessage(
{
id: "myStay.cancelStay.roomCancelledToastMessage",
defaultMessage: "{roomName} room was cancelled",
},
{ roomName: cancelledRoom.roomName }
)}
</strong>
</span>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.textDefault}>
{intl.formatMessage({
id: "myStay.cancelStay.stayStillActiveToastMessage",
defaultMessage:
"Your Stay is still active with the other room",
})}
</span>
</Typography>
</div>
)
}
}
} else {
toast.warning(
intl.formatMessage({
id: "myStay.cancelStay.partialCancellationErrorMessage",
defaultMessage:
"Some rooms were cancelled successfully, but we encountered issues with others. Please contact customer service for assistance.",
})
)
}
utils.booking.get.invalidate({
refId: bookedRoom.refId,
})
utils.booking.linkedReservations.invalidate({
lang,
refId: bookedRoom.refId,
})
closeModal()
},
onError() {
toast.error(
intl.formatMessage({
id: "myStay.cancelStay.cancellationErrorMessage",
defaultMessage: "Something went wrong. Please try again later.",
})
)
},
})
function cancelBooking() {
if (Array.isArray(formRooms)) {
const refIdsToCancel = formRooms
.filter((r) => r.checked)
.map((r) => r.refId)
if (refIdsToCancel.length) {
cancelBookingsMutation.mutate({
refIds: refIdsToCancel,
language: lang,
})
}
} else {
toast.error(
intl.formatMessage({
id: "myStay.cancelStay.cancellationErrorMessage",
defaultMessage: "Something went wrong. Please try again later.",
})
)
}
}
const confirm = intl.formatMessage({
id: "myStay.cancelStay.confirmCancellation",
defaultMessage: "Confirm cancellation",
})
const dontCancel = intl.formatMessage({
id: "myStay.cancelStay.dontCancel",
defaultMessage: "Don't cancel",
})
const text = intl.formatMessage({
id: "myStay.cancelStay.finalConfirmationText",
defaultMessage: "Are you sure you want to continue with the cancellation?",
})
const title = intl.formatMessage({
id: "common.cancelBooking",
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,
refId: room.refId,
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,29 @@
.dialog {
max-width: 690px;
}
.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(--Background-Primary);
border-radius: var(--Corner-radius-md);
align-items: center;
gap: var(--Spacing-x1);
}
.roomInfo {
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,42 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import Alerts from "./Alerts"
import Steps from "./Steps"
import styles from "./cancelStay.module.css"
export default function CancelStay() {
const intl = useIntl()
function trackCancelStay() {
trackMyStayPageLink("cancel booking")
}
return (
<DialogTrigger>
<ActionsButton
icon="cancel"
onPress={trackCancelStay}
text={intl.formatMessage({
id: "booking.cancelStay",
defaultMessage: "Cancel stay",
})}
/>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (
<Alerts closeModal={close}>
<Steps closeModal={close} />
</Alerts>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
import Modal from "@/components/HotelReservation/MyStay/Modal"
export default function CannotChangeDate({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
id: "myStay.actions.changeDates",
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.contactCustomerService",
defaultMessage: "Contact customer service",
})}
text={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.contactCustomerService.text",
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", id: "common.back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
import Modal from "@/components/HotelReservation/MyStay/Modal"
export default function MultiRoomBooking({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
id: "myStay.actions.changeDates",
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.contactCustomerService",
defaultMessage: "Contact customer service",
})}
text={intl.formatMessage({
id: "myStay.actions.contactBooker.multiroom.update",
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", id: "common.back" })}
</Modal.Content.Footer.Secondary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
import Modal from "@/components/HotelReservation/MyStay/Modal"
export default function NotMainRoom({
closeModal,
}: {
closeModal: () => void
}) {
const intl = useIntl()
return (
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
id: "myStay.actions.changeDates",
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "myStay.actions.contactBooker",
defaultMessage: "Contact the person who booked the stay",
})}
text={intl.formatMessage({
id: "myStay.actions.contactBooker.multiroom.updateDates",
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", id: "common.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,66 @@
"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({
id: "common.checkIn",
defaultMessage: "Check-in",
})
const checkOutMsg = intl.formatMessage({
id: "common.checkOut",
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,198 @@
"use client"
import { useIntl } from "react-intl"
import { longDateWithYearFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { toast } from "@scandic-hotels/design-system/Toast"
import { trpc } from "@scandic-hotels/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import PriceContainer from "@/components/HotelReservation/MyStay/ReferenceCard/PriceContainer"
import useLang from "@/hooks/useLang"
import PriceAndDate from "./PriceAndDate"
import styles from "./confirmation.module.css"
import type { Lang } from "@scandic-hotels/common/constants/language"
interface ConfirmationProps {
checkInDate: string
checkOutDate: string
closeModal: () => void
newPrice: string
}
function formatDate(date: Date | string, lang: Lang) {
return dt(date).locale(lang).format(longDateWithYearFormat[lang])
}
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({
refId: updatedBooking.refId,
})
toast.success(
intl.formatMessage({
id: "myStay.changeDates.stayUpdatedToastMessage",
defaultMessage: "Your stay was updated",
})
)
closeModal()
} else {
toast.error(
intl.formatMessage({
id: "myStay.changeDates.stayUpdateFailedToastMessage",
defaultMessage: "Failed to update your stay",
})
)
}
},
onError: () => {
toast.error(
intl.formatMessage({
id: "myStay.changeDates.stayUpdateFailedToastMessage",
defaultMessage: "Failed to update your stay",
})
)
},
})
function handleModifyStay() {
updateBooking.mutate({
refId: bookedRoom.refId,
checkInDate,
checkOutDate,
language: lang,
})
}
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(
{
id: "booking.numberOfNights",
defaultMessage: "{totalNights, plural, one {# night} other {# nights}}",
},
{ totalNights: nights }
)
const newDatesLabel = intl.formatMessage({
id: "myStay.changeDates.newDatesLabel",
defaultMessage: "New dates",
})
const oldDatesLabel = intl.formatMessage({
id: "myStay.changeDates.oldDatesLabel",
defaultMessage: "Old dates",
})
const title = intl.formatMessage({
id: "myStay.changeDates.confirmationTitle",
defaultMessage: "Confirm date change",
})
const totalDueMsg = intl.formatMessage({
id: "myStay.changeDates.payAtHotelMessage",
defaultMessage: "Pay at the hotel",
})
const adultsText = intl.formatMessage(
{
id: "booking.numberOfAdults",
defaultMessage: "{adults, plural, one {# adult} other {# adults}}",
},
{ adults: totalAdults }
)
const childrenText = intl.formatMessage(
{
id: "booking.numberOfChildren",
defaultMessage: "{children, plural, one {# child} other {# children}}",
},
{ children: 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="Border/Divider/Subtle" />
<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", id: "common.back" })}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary
disabled={updateBooking.isPending}
onClick={handleModifyStay}
>
{intl.formatMessage({
defaultMessage: "Confirm",
id: "common.confirm",
})}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)
}

View File

@@ -0,0 +1,22 @@
"use client"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { Alert } from "@scandic-hotels/design-system/Alert"
export default function NoAvailability() {
const intl = useIntl()
return (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({
id: "booking.noAvailability",
defaultMessage: "No availability",
})}
text={intl.formatMessage({
id: "myStay.referenceCard.actions.changeDates.noAvailability.text",
defaultMessage: "No single rooms are available on these dates",
})}
/>
)
}

View File

@@ -0,0 +1,155 @@
"use client"
import {
Button as ButtonRAC,
Dialog,
DialogTrigger,
} from "react-aria-components"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import { longDateWithYearFormat } from "@scandic-hotels/common/constants/dateFormats"
import { dt } from "@scandic-hotels/common/dt"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import DatePickerSingleDesktop from "@/components/DatePicker/Single/Desktop"
import DatePickerSingleMobile from "@/components/DatePicker/Single/Mobile"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import useLang from "@/hooks/useLang"
import styles from "./newDates.module.css"
interface NewDatesProps {
checkInDate: string
checkOutDate: string
}
export default function NewDates({ checkInDate, checkOutDate }: NewDatesProps) {
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")
const fromDate = useWatch({ name: "checkInDate" })
const toDate = useWatch({ name: "checkOutDate" })
function handleSelectDate(date: Date, name: "checkInDate" | "checkOutDate") {
setValue(name, dt(date).format("YYYY-MM-DD"))
}
function handleSelectCheckInDate(checkIn: Date) {
handleSelectDate(checkIn, "checkInDate")
if (dt(checkIn).isSameOrAfter(toDate)) {
handleSelectDate(
dt(checkIn).add(defaultDaysBetween, "days").toDate(),
"checkOutDate"
)
}
}
function handleSelectCheckOutDate(checkOut: Date) {
handleSelectDate(checkOut, "checkOutDate")
if (dt(checkOut).isSameOrBefore(fromDate)) {
handleSelectDate(
dt(checkOut).subtract(defaultDaysBetween, "days").toDate(),
"checkInDate"
)
}
}
const checkInLabel = intl.formatMessage({
id: "common.checkIn",
defaultMessage: "Check-in",
})
const checkOutLabel = intl.formatMessage({
id: "common.checkOut",
defaultMessage: "Check-out",
})
const checkInText = dt(fromDate)
.locale(lang)
.format(longDateWithYearFormat[lang])
const checkOutText = dt(toDate)
.locale(lang)
.format(longDateWithYearFormat[lang])
return (
<>
<div className={styles.container}>
<div className={styles.checkInDate}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.textDefault}>{checkInLabel}</span>
</Typography>
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{checkInText}</span>
</Typography>
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
<Modal>
<Dialog>
{({ close }) => (
<>
<DatePickerSingleDesktop
close={close}
handleOnSelect={handleSelectCheckInDate}
selectedDate={fromDate}
startMonth={fromDate}
/>
<DatePickerSingleMobile
close={close}
handleOnSelect={handleSelectCheckInDate}
hideHeader
selectedDate={fromDate}
/>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
</div>
<div className={styles.checkOutDate}>
<Typography variant="Body/Supporting text (caption)/smBold">
<span className={styles.textDefault}>{checkOutLabel}</span>
</Typography>
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<Typography variant="Body/Paragraph/mdRegular">
<span>{checkOutText}</span>
</Typography>
<MaterialIcon icon="calendar_today" />
</ButtonRAC>
<Modal>
<Dialog>
{({ close }) => (
<>
<DatePickerSingleDesktop
close={close}
handleOnSelect={handleSelectCheckOutDate}
selectedDate={toDate}
startMonth={toDate}
/>
<DatePickerSingleMobile
close={close}
handleOnSelect={handleSelectCheckOutDate}
hideHeader
selectedDate={toDate}
/>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,35 @@
.container {
background-color: var(--Background-Primary);
padding: var(--Spacing-x2) var(--Spacing-x3) var(--Spacing-x3);
border-radius: var(--Corner-radius-md);
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
}
.checkInDate,
.checkOutDate {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}
.trigger {
align-items: center;
background-color: var(--Main-Grey-White);
border-color: var(--Scandic-Beige-40);
border-radius: var(--Corner-radius-md);
border-style: solid;
border-width: 1px;
display: flex;
height: 60px;
justify-content: space-between;
min-width: 0;
/* allow shrinkage */
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
}
.textDefault {
color: var(--Text-Default);
}

View File

@@ -0,0 +1,92 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { dt } from "@scandic-hotels/common/dt"
import { toast } from "@scandic-hotels/design-system/Toast"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
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({
id: "myStay.actions.changeDates.selectDate.errorMessage",
defaultMessage: "Please select dates",
})
)
}
}
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(handleSubmit)}>
<Modal.Content>
<Modal.Content.Header
handleClose={closeModal}
title={intl.formatMessage({
id: "myStay.actions.changeDates",
defaultMessage: "New dates for the stay",
})}
/>
<Modal.Content.Body>
{noAvailability && <NoAvailability />}
<NewDates checkInDate={checkInDate} checkOutDate={checkOutDate} />
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={closeModal}>
{intl.formatMessage({
defaultMessage: "Back",
id: "common.back",
})}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary
disabled={methods.formState.isSubmitting}
intent="secondary"
type="submit"
>
{intl.formatMessage({
id: "myStay.actions.changeDates.checkAvailability",
defaultMessage: "Check availability",
})}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
</form>
</FormProvider>
)
}

View File

@@ -0,0 +1,140 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { trpc } from "@scandic-hotels/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import useLang from "@/hooks/useLang"
import Confirmation from "./Confirmation"
import Form from "./Form"
import type { ChangeDatesStepsProps } from "@/types/components/hotelReservation/myStay/changeDates"
interface Dates {
fromDate: string
toDate: string
}
export default function Steps({ closeModal }: ChangeDatesStepsProps) {
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, isLoggedIn } =
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,
},
isLoggedIn: state.isLoggedIn,
}))
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 && 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.numberOfCheques
) {
const { additionalPricePerStay, currency, numberOfCheques } =
data.product.corporateCheque.localPrice
setNewPrice(
formatPrice(
intl,
numberOfCheques,
CurrencyEnum.CC,
additionalPricePerStay + extraPrice,
currency?.toString() ?? pkgsSum.currency ?? currencyCode
)
)
} else if (
"voucher" in data.product &&
data.product.voucher.numberOfVouchers
) {
const { numberOfVouchers } = data.product.voucher
setNewPrice(formatPrice(intl, numberOfVouchers, CurrencyEnum.Voucher))
} 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,61 @@
"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/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
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)
function trackChangeDates() {
trackMyStayPageLink("modify dates")
}
const text = intl.formatMessage({
defaultMessage: "Change dates",
id: "myStay.referenceCard.actions.changeDates",
})
return (
<DialogTrigger>
<ActionsButton
icon="edit_calendar"
isDisabled={isDisabled}
onPress={trackChangeDates}
text={text}
/>
<Modal>
<Dialog>
{({ close }) => (
<Alerts closeModal={close}>
<Steps closeModal={close} />
</Alerts>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,30 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import CustomerSupportModal from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/CustomerSupportModal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
export default function CustomerSupport() {
const intl = useIntl()
function trackCustomerSupport() {
trackMyStayPageLink("customer service")
}
return (
<DialogTrigger>
<ActionsButton
onPress={trackCustomerSupport}
icon="support_agent"
text={intl.formatMessage({
id: "common.customerSupport",
defaultMessage: "Customer support",
})}
/>
<CustomerSupportModal />
</DialogTrigger>
)
}

View File

@@ -0,0 +1,54 @@
.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-md);
display: flex;
gap: var(--Spacing-x3);
justify-content: flex-end;
padding: var(--Spacing-x2);
}
.guaranteeCostText {
display: flex;
flex-direction: column;
}
.baseTextHighContrast {
color: var(--Base-Text-High-contrast);
white-space: nowrap;
}
.textDefault {
color: var(--Text-Default);
}
@media screen and (min-width: 768px) {
.guaranteeCostText {
align-items: flex-end;
}
}

View File

@@ -0,0 +1,231 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { usePathname } from "next/navigation"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { writeGlaToSessionStorage } from "@scandic-hotels/booking-flow/components/EnterDetails/Payment/PaymentCallback/helpers"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Divider } from "@scandic-hotels/design-system/Divider"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import Link from "@scandic-hotels/design-system/OldDSLink"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
import { isWebview } from "@/constants/routes/webviews"
import { env } from "@/env/client"
import { useMyStayStore } from "@/stores/my-stay"
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
import useLang from "@/hooks/useLang"
import { trackUpdatePaymentMethod } from "@/utils/tracking"
import { type GuaranteeFormData, paymentSchema } from "./schema"
import styles from "./form.module.css"
export default function Form() {
const intl = useIntl()
const lang = useLang()
const pathname = usePathname()
const { confirmationNumber, currencyCode, hotelId, refId, savedCreditCards } =
useMyStayStore((state) => ({
confirmationNumber: state.bookedRoom.confirmationNumber,
currencyCode: state.bookedRoom.currencyCode,
hotelId: state.bookedRoom.hotelId,
refId: state.bookedRoom.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, isWebview(pathname))}`
const { guaranteeBooking, isLoading, handleGuaranteeError } =
useGuaranteeBooking(refId, false, hotelId)
if (isLoading) {
return (
<div className={styles.loading}>
<LoadingSpinner />
</div>
)
}
function handleGuaranteeLateArrival(data: GuaranteeFormData) {
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
trackGlaSaveCardAttempt({
hotelId,
hasSavedCreditCard: !!savedCreditCard,
lateArrivalGuarantee: "yes",
})
if (confirmationNumber) {
const card = savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined
const lateArrivalGuarantee = "yes"
writeGlaToSessionStorage(
lateArrivalGuarantee,
hotelId,
savedCreditCard ? savedCreditCard.type : PaymentMethodEnum.card,
!!savedCreditCard
)
guaranteeBooking.mutate({
refId,
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({
id: "errorMessage.somethingWentWrong",
defaultMessage: "Something went wrong!",
})
)
}
}
const guaranteeMsg = intl.formatMessage(
{
id: "myStay.gla.termsAndConditionsMessage",
defaultMessage:
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>.",
},
{
termsAndConditionsLink: (str) => (
<Link
textDecoration="underline"
color="Text/Interactive/Secondary"
target="_blank"
href={bookingTermsAndConditionsRoutes[lang]}
onClick={(e) => e.stopPropagation()}
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
textDecoration="underline"
color="Text/Interactive/Secondary"
target="_blank"
href={privacyPolicyRoutes[lang]}
onClick={(e) => e.stopPropagation()}
>
{str}
</Link>
),
}
)
return (
<FormProvider {...methods}>
<form
className={styles.form}
id="guarantee"
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
>
{savedCreditCards?.length ? (
<SelectPaymentMethod
formName="paymentMethod"
paymentMethods={savedCreditCards.map((x) => ({
...x,
cardType: x.type as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
/>
) : null}
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl
.formatMessage({
id: "common.other",
defaultMessage: "Other",
})
.toUpperCase()
: undefined
}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
id: "common.creditCard",
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({
id: "booking.totalDue",
defaultMessage: "Total due",
})}
</span>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={styles.textDefault}>
{intl.formatMessage({
id: "myStay.gla.chargeInfo",
defaultMessage:
"Your card will only be charged in the event of a no-show",
})}
</span>
</Typography>
</div>
<Divider variant="vertical" />
<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 type GuaranteeFormData = z.output<typeof paymentSchema>

View File

@@ -0,0 +1,95 @@
"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/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import { dateHasPassed } from "../utils"
import Form from "./Form"
import styles from "./guarantee.module.css"
export default function GuaranteeLateArrival() {
const intl = useIntl()
const { checkInDate, checkInTime, guaranteeInfo, isCancelled, priceType } =
useMyStayStore((state) => ({
checkInDate: state.bookedRoom.checkInDate,
checkInTime: state.hotel.hotelFacts.checkin.checkInTime,
guaranteeInfo: state.bookedRoom.guaranteeInfo,
isCancelled: state.bookedRoom.isCancelled,
priceType: state.bookedRoom.priceType,
}))
const isRewardNight = priceType === "points"
const guaranteeable =
!guaranteeInfo &&
!isCancelled &&
!dateHasPassed(checkInDate, checkInTime) &&
!isRewardNight
if (!guaranteeable) {
return null
}
function trackGuaranteeLateArrival() {
trackMyStayPageLink("guarantee late arrival")
}
const arriveLateMsg = intl.formatMessage({
id: "myStay.gla.arriveLateMessage",
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({
id: "myStay.gla.heading",
defaultMessage: "Guarantee late arrival",
})
return (
<DialogTrigger>
<ActionsButton
onPress={trackGuaranteeLateArrival}
text={text}
icon="check"
/>
<Modal>
<Dialog className={styles.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({
id: "common.back",
defaultMessage: "Back",
})}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary form="guarantee" type="submit">
{intl.formatMessage({
id: "myStay.gla.guarantee",
defaultMessage: "Guarantee",
})}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,73 @@
"use client"
import { useIntl } from "react-intl"
import { logger } from "@scandic-hotels/common/logger"
import { toast } from "@scandic-hotels/design-system/Toast"
import { trpc } from "@scandic-hotels/trpc/client"
import { useMyStayStore } from "@/stores/my-stay"
import useLang from "@/hooks/useLang"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
type ResendConfirmationEmailProps = {
onClose: () => void
}
export default function ResendConfirmationEmail({
onClose,
}: ResendConfirmationEmailProps) {
const intl = useIntl()
const lang = useLang()
const refId = useMyStayStore((state) => state.refId)
const resendEmail = trpc.booking.resendConfirmation.useMutation()
function resendConfirmationEmail() {
trackMyStayPageLink("resend confirmation email")
resendEmail.mutate(
{ language: lang, refId },
{
onSuccess() {
onClose()
toast.success(
intl.formatMessage({
id: "myStay.manageStay.resendConfirmationEmail.success",
defaultMessage: "Confirmation email was resent successfully",
})
)
},
onError(e) {
onClose()
toast.error(
intl.formatMessage({
id: "myStay.manageStay.resendConfirmationEmail.error",
defaultMessage:
"There was an error resending the confirmation email",
})
)
logger.error("[myStay] Resend confirmation email failed", {
error: e.data,
})
},
}
)
}
const printMsg = intl.formatMessage({
id: "myStay.manageStay.resendConfirmationEmail",
defaultMessage: "Resend confirmation email",
})
return (
<ActionsButton
onPress={resendConfirmationEmail}
isDisabled={resendEmail.isPending}
icon="email"
text={printMsg}
/>
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import { useIntl } from "react-intl"
import { CancellationRuleEnum } from "@scandic-hotels/common/constants/booking"
import { preliminaryReceipt } from "@scandic-hotels/common/constants/routes/myStay"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Link from "@scandic-hotels/design-system/OldDSLink"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
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({
id: "myStay.manageStay.viewReceipt",
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-x05) 0;
}

View File

@@ -0,0 +1,8 @@
.list {
display: flex;
flex-direction: column;
align-items: start;
margin: 0;
padding: 0;
gap: var(--Space-x15);
}

View File

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

View File

@@ -0,0 +1,7 @@
import { dt } from "@scandic-hotels/common/dt"
export function dateHasPassed(date: string, 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,47 @@
"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({
id: "common.bookingNumber",
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,34 @@
.container {
align-items: flex-start;
background-color: var(--Surface-Primary-OnSurface-Default);
border-radius: var(--Corner-radius-md);
display: flex;
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;
}
@media screen and (min-width: 768px) {
.container {
flex-direction: column;
}
}

View File

@@ -0,0 +1,57 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
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 { Typography } from "@scandic-hotels/design-system/Typography"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import Actions from "./Actions"
import Info from "./Info"
import styles from "./manageStay.module.css"
export default function ManageStay() {
const intl = useIntl()
const manageStay = intl.formatMessage({
id: "myStay.manageStay.manageStay",
defaultMessage: "Manage stay",
})
return (
<DialogTrigger>
<Button
size="Medium"
variant="Tertiary"
typography="Body/Supporting text (caption)/smBold"
>
<span>{manageStay}</span>
<MaterialIcon color="CurrentColor" icon="keyboard_arrow_down" />
</Button>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (
<>
<header className={styles.header}>
<Typography variant="Title/Subtitle/lg">
<span className={styles.title}>{manageStay}</span>
</Typography>
<IconButton onPress={close} theme="Inverted">
<MaterialIcon color="CurrentColor" icon="close" />
</IconButton>
</header>
<div className={styles.content}>
<Actions onClose={close} />
<Info />
</div>
</>
)}
</Dialog>
</Modal>
</DialogTrigger>
)
}

View File

@@ -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;
}
}

View File

@@ -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>
</>
)
}